diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 0000000..5c61531 --- /dev/null +++ b/.claudeignore @@ -0,0 +1,32 @@ +# Build outputs +dist/ + +# Dependencies +node_modules/ + +# Lock files +package-lock.json +yarn.lock + +# Test coverage output +coverage/ + +# Generated type declarations +**/*.d.ts +*.tsbuildinfo + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# E2E test fixtures (keep unit tests) +test/ + +# Environment secrets — never read +.env +.env.* +!.env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index fb41b17..0000000 --- a/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Server -PORT=4100 -CORS_ORIGIN=http://localhost:5173 - -# Fortytwo Platform -PLATFORM_GRAPHQL_URL=http://localhost:4000/graphql -PLATFORM_API_KEY= - -# Exotel -EXOTEL_API_KEY= -EXOTEL_API_TOKEN= -EXOTEL_ACCOUNT_SID= -EXOTEL_SUBDOMAIN=api.exotel.com -EXOTEL_WEBHOOK_SECRET= - -# AI -ANTHROPIC_API_KEY= diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierrc b/.prettierrc index a20502b..dcb7279 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { "singleQuote": true, "trailingComma": "all" -} +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 4e9f827..e06647a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,6 +29,16 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn', + '@typescript-eslint/no-unsafe-assignment': 'warn', + '@typescript-eslint/no-unsafe-call': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'warn', + '@typescript-eslint/no-unsafe-return': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-base-to-string': 'warn', + '@typescript-eslint/no-misused-promises': 'warn', + '@typescript-eslint/require-await': 'warn', + '@typescript-eslint/no-redundant-type-constituents': 'warn', + 'no-empty': 'warn', "prettier/prettier": ["error", { endOfLine: "auto" }], }, }, diff --git a/package-lock.json b/package-lock.json index 4e2dfd5..2456c0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.17", + "@nestjs/swagger": "^11.2.6", "@nestjs/websockets": "^11.1.17", "ai": "^6.0.116", "axios": "^1.13.6", @@ -38,7 +39,9 @@ "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", "globals": "^16.0.0", + "husky": "^9.1.7", "jest": "^30.0.0", + "lint-staged": "^16.4.0", "prettier": "^3.4.2", "source-map-support": "^0.5.21", "supertest": "^7.0.0", @@ -2583,6 +2586,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "http://localhost:4873/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2899,6 +2908,26 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.17", "resolved": "http://localhost:4873/@nestjs/platform-express/-/platform-express-11.1.17.tgz", @@ -3037,6 +3066,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.6.tgz", + "integrity": "sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.1", + "lodash": "4.17.23", + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.31.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.17", "resolved": "http://localhost:4873/@nestjs/testing/-/testing-11.1.17.tgz", @@ -3160,6 +3222,13 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "http://localhost:4873/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -4554,7 +4623,6 @@ "version": "2.0.1", "resolved": "http://localhost:4873/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -5100,6 +5168,69 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "http://localhost:4873/cli-width/-/cli-width-4.1.0.tgz", @@ -5191,6 +5322,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "http://localhost:4873/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5696,6 +5834,19 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "http://localhost:4873/error-ex/-/error-ex-1.3.4.tgz", @@ -6053,6 +6204,13 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "http://localhost:4873/events/-/events-3.3.0.tgz", @@ -6567,6 +6725,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "http://localhost:4873/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6867,6 +7038,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "http://localhost:4873/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -7986,7 +8173,6 @@ "version": "4.1.1", "resolved": "http://localhost:4873/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8116,6 +8302,156 @@ "dev": true, "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-esm": { "version": "1.0.3", "resolved": "http://localhost:4873/load-esm/-/load-esm-1.0.3.tgz", @@ -8202,6 +8538,209 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "http://localhost:4873/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8390,6 +8929,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "http://localhost:4873/minimatch/-/minimatch-3.1.5.tgz", @@ -9256,6 +9808,13 @@ "dev": true, "license": "ISC" }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "http://localhost:4873/router/-/router-2.2.0.tgz", @@ -9508,6 +10067,52 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/socket.io": { "version": "4.8.3", "resolved": "http://localhost:4873/socket.io/-/socket.io-4.8.3.tgz", @@ -9679,6 +10284,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "http://localhost:4873/string-length/-/string-length-4.0.2.tgz", @@ -9849,6 +10464,15 @@ "node": ">=8" } }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "http://localhost:4873/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -10092,6 +10716,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "http://localhost:4873/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -11010,6 +11644,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "http://localhost:4873/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index a61c058..4102edf 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "prepare": "husky" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.58", @@ -28,6 +29,7 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.17", + "@nestjs/swagger": "^11.2.6", "@nestjs/websockets": "^11.1.17", "ai": "^6.0.116", "axios": "^1.13.6", @@ -49,7 +51,9 @@ "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", "globals": "^16.0.0", + "husky": "^9.1.7", "jest": "^30.0.0", + "lint-staged": "^16.4.0", "prettier": "^3.4.2", "source-map-support": "^0.5.21", "supertest": "^7.0.0", @@ -61,6 +65,10 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.20.0" }, + "lint-staged": { + "src/**/*.ts": ["eslint --fix", "prettier --write"], + "test/**/*.ts": ["eslint --fix", "prettier --write"] + }, "jest": { "moduleFileExtensions": [ "js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..215424d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6909 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/anthropic': + specifier: ^3.0.58 + version: 3.0.63(zod@4.3.6) + '@ai-sdk/openai': + specifier: ^3.0.41 + version: 3.0.47(zod@4.3.6) + '@deepgram/sdk': + specifier: ^5.0.0 + version: 5.0.0 + '@nestjs/common': + specifier: ^11.0.1 + version: 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.3 + version: 4.0.3(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.1 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.0.1 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + '@nestjs/platform-socket.io': + specifier: ^11.1.17 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(rxjs@7.8.2) + '@nestjs/websockets': + specifier: ^11.1.17 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + ai: + specifier: ^6.0.116 + version: 6.0.134(zod@4.3.6) + axios: + specifier: ^1.13.6 + version: 1.13.6 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + socket.io: + specifier: ^4.8.3 + version: 4.8.3 + devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.3.5 + '@eslint/js': + specifier: ^9.18.0 + version: 9.39.4 + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.16(@types/node@22.19.15) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.0.1 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17) + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^22.10.7 + version: 22.19.15 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.3 + eslint: + specifier: ^9.18.0 + version: 9.39.4 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.4) + eslint-plugin-prettier: + specifier: ^5.2.2 + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.1) + globals: + specifier: ^16.0.0 + version: 16.5.0 + jest: + specifier: ^30.0.0 + version: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + prettier: + specifier: ^3.4.2 + version: 3.8.1 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.2.2 + ts-jest: + specifier: ^29.2.5 + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) + ts-loader: + specifier: ^9.5.2 + version: 9.5.4(typescript@5.9.3)(webpack@5.104.1) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.19.15)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.20.0 + version: 8.57.1(eslint@9.39.4)(typescript@5.9.3) + +packages: + + '@ai-sdk/anthropic@3.0.63': + resolution: {integrity: sha512-SiLosFr0FfKfrNpAAj8mD/i3S5YBB/z5orb1DH3pN1yATuBNjjPMLnRE4P3Dn7Y5cQsro0uzw5g5117hkShWoQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@3.0.77': + resolution: {integrity: sha512-UdwIG2H2YMuntJQ5L+EmED5XiwnlvDT3HOmKfVFxR4Nq/RSLFA/HcchhwfNXHZ5UJjyuL2VO0huLbWSZ9ijemQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.47': + resolution: {integrity: sha512-bRsb2sDN5u+pKO3Kdr0flpxtL+cPwQ2uCo/pVyzIbj2I4AkKAokJHhw5JWLVOeEwdlYzWfmv+hzaiGarzUcTFQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.21': + resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + + '@angular-devkit/core@19.2.17': + resolution: {integrity: sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.19': + resolution: {integrity: sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.19': + resolution: {integrity: sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.17': + resolution: {integrity: sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.19': + resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@deepgram/sdk@5.0.0': + resolution: {integrity: sha512-x1wMiOgDGqcLEaQpQBQLTtk5mLbXbYgcBEpp7cfJIyEtqdIGgijCZH+a/esiVp+xIcTYYroTxG47RVppZOHbWw==} + engines: {node: '>=18.0.0'} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@30.3.0': + resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.3.0': + resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.3.0': + resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.3.0': + resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.3.0': + resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.3.0': + resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.3.0': + resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.3.0': + resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.3.0': + resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.3.0': + resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@nestjs/cli@11.0.16': + resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.1.17': + resolution: {integrity: sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@4.0.3': + resolution: {integrity: sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@11.1.17': + resolution: {integrity: sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/platform-express@11.1.17': + resolution: {integrity: sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/platform-socket.io@11.1.17': + resolution: {integrity: sha512-BSOAsENdmTtsnDL0hb4takbWzPy9WoPybjlM57ab3/rQgm0biMFYUupH2uzmCjmmIXJL/EFbAWznVl8xw2Sa6Q==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/schematics@11.0.9': + resolution: {integrity: sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==} + peerDependencies: + typescript: '>=4.8.2' + + '@nestjs/testing@11.1.17': + resolution: {integrity: sha512-lNffw+z+2USewmw4W0tsK+Rq94A2N4PiHbcqoRUu5y8fnqxQeIWGHhjo5BFCqj7eivqJBhT7WdRydxVq4rAHzg==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nestjs/websockets@11.1.17': + resolution: {integrity: sha512-YbwQ0QfVj0lxkKQhdIIgk14ZSVWDqGk1J8nNSN6SLjf36sVv58Ma5ro+dtQua8wj3l2Ub7JJCVFixEhKtYc/rQ==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/platform-socket.io': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} + hasBin: true + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@15.1.1': + resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.57.1': + resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.57.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.57.1': + resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.57.1': + resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.57.1': + resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.1': + resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.57.1': + resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.57.1': + resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.1': + resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.57.1': + resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.57.1': + resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ai@6.0.134: + resolution: {integrity: sha512-YalNEaavld/kE444gOcsMKXdVVRGEe0SK77fAFcWYcqLg+a7xKnEet8bdfrEAJTfnMjj01rhgrIL10903w1a5Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + babel-jest@30.3.0: + resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.3.0: + resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.3.0: + resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + baseline-browser-mapping@2.10.10: + resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@4.4.1: + resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} + engines: {node: '>= 6'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.321: + resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.6: + resolution: {integrity: sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.3.0: + resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-type@21.3.2: + resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==} + engines: {node: '>=20'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} + engines: {node: 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@30.3.0: + resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.3.0: + resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.3.0: + resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.3.0: + resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.3.0: + resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.3.0: + resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.3.0: + resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.3.0: + resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.3.0: + resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.3.0: + resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.3.0: + resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.3.0: + resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.3.0: + resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.3.0: + resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.3.0: + resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@30.3.0: + resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.3.0: + resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-loader@9.5.4: + resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.57.1: + resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} + engines: {node: '>=10.13.0'} + + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + 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 + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@ai-sdk/anthropic@3.0.63(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/gateway@3.0.77(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 + + '@ai-sdk/openai@3.0.47(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/provider-utils@4.0.21(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + + '@angular-devkit/core@19.2.17(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.19(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.19(@types/node@22.19.15)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@22.19.15) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.17(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.19(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@borewit/text-codec@0.2.2': {} + + '@colors/colors@1.5.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@deepgram/sdk@5.0.0': + dependencies: + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.19.15)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.15) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/confirm@5.1.21(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/core@10.3.2(@types/node@22.19.15)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.15) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/editor@4.2.23(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/expand@4.0.23(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/external-editor@1.0.3(@types/node@22.19.15)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/number@3.0.23(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/password@4.0.23(@types/node@22.19.15)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/prompts@7.10.1(@types/node@22.19.15)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.15) + '@inquirer/confirm': 5.1.21(@types/node@22.19.15) + '@inquirer/editor': 4.2.23(@types/node@22.19.15) + '@inquirer/expand': 4.0.23(@types/node@22.19.15) + '@inquirer/input': 4.3.1(@types/node@22.19.15) + '@inquirer/number': 3.0.23(@types/node@22.19.15) + '@inquirer/password': 4.0.23(@types/node@22.19.15) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.15) + '@inquirer/search': 3.2.2(@types/node@22.19.15) + '@inquirer/select': 4.4.2(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/prompts@7.3.2(@types/node@22.19.15)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.15) + '@inquirer/confirm': 5.1.21(@types/node@22.19.15) + '@inquirer/editor': 4.2.23(@types/node@22.19.15) + '@inquirer/expand': 4.0.23(@types/node@22.19.15) + '@inquirer/input': 4.3.1(@types/node@22.19.15) + '@inquirer/number': 3.0.23(@types/node@22.19.15) + '@inquirer/password': 4.0.23(@types/node@22.19.15) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.15) + '@inquirer/search': 3.2.2(@types/node@22.19.15) + '@inquirer/select': 4.4.2(@types/node@22.19.15) + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/rawlist@4.1.11(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/type': 3.0.10(@types/node@22.19.15) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/search@3.2.2(@types/node@22.19.15)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.15) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/select@4.4.2(@types/node@22.19.15)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.15) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.15) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.15 + + '@inquirer/type@3.0.10(@types/node@22.19.15)': + optionalDependencies: + '@types/node': 22.19.15 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + + '@jest/core@30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.3.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.3.0 + jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.3.0': {} + + '@jest/environment@30.3.0': + dependencies: + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + jest-mock: 30.3.0 + + '@jest/expect-utils@30.3.0': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.3.0': + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.1.1 + '@types/node': 22.19.15 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.3.0': + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/types': 30.3.0 + jest-mock: 30.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 22.19.15 + jest-regex-util: 30.0.1 + + '@jest/reporters@30.3.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.19.15 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + + '@jest/snapshot-utils@30.3.0': + dependencies: + '@jest/types': 30.3.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.3.0': + dependencies: + '@jest/console': 30.3.0 + '@jest/types': 30.3.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.3.0': + dependencies: + '@jest/test-result': 30.3.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + slash: 3.0.0 + + '@jest/transform@30.3.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.15 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lukeed/csprng@1.1.0': {} + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nestjs/cli@11.0.16(@types/node@22.19.15)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.19(@types/node@22.19.15)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@22.19.15) + '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1) + glob: 13.0.0 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.104.1 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@types/node' + - esbuild + - uglify-js + - webpack-cli + + '@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.2 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@4.0.3(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.2.3 + dotenv-expand: 12.0.3 + lodash: 4.17.23 + rxjs: 7.8.2 + + '@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.3.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + '@nestjs/websockets': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + + '@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': + dependencies: + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/platform-socket.io@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + rxjs: 7.8.2 + socket.io: 4.8.3 + tslib: 2.8.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + comment-json: 4.4.1 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17)': + dependencies: + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) + + '@nestjs/websockets@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-socket.io': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.17)(rxjs@7.8.2) + + '@noble/hashes@1.8.0': {} + + '@nuxt/opencollective@0.4.1': + dependencies: + consola: 3.4.2 + + '@opentelemetry/api@1.9.0': {} + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@sinclair/typebox@0.34.48': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@15.1.1': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@socket.io/component-emitter@3.1.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.15 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.15 + + '@types/cookiejar@2.1.5': {} + + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.19.15 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 22.19.15 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.3.0 + pretty-format: 30.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/methods@1.1.4': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.15 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.15 + + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.19.15 + form-data: 4.0.5 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.15 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/type-utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.1(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.57.1': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.57.1': {} + + '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.1(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.57.1': + dependencies: + '@typescript-eslint/types': 8.57.1 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vercel/oidc@3.1.0': {} + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ai@6.0.134(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.77(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 + + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.14.0): + dependencies: + ajv: 6.14.0 + + ajv-keywords@5.1.0(ajv@8.18.0): + dependencies: + ajv: 8.18.0 + fast-deep-equal: 3.1.3 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + append-field@1.0.0: {} + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + asynckit@0.4.0: {} + + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.3.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + baseline-browser-mapping@2.10.10: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.321 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001781: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + ci-info@4.4.0: {} + + cjs-module-lexer@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + commander@4.1.1: {} + + comment-json@4.4.1: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + consola@3.4.2: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookiejar@2.1.4: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + detect-newline@3.1.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff@4.0.4: {} + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.321: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + engine.io-parser@5.2.3: {} + + engine.io@6.6.6: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 22.19.15 + '@types/ws': 8.18.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.6 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.1): + dependencies: + eslint: 9.39.4 + prettier: 3.8.1 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 10.1.8(eslint@9.39.4) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + eventsource-parser@3.0.6: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.3.0: + dependencies: + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.0: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-type@21.3.2: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1): + dependencies: + '@babel/code-frame': 7.29.0 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.5 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.4 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.104.1 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.0: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + globals@16.5.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + human-signals@2.1.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@30.3.0: + dependencies: + execa: 5.1.1 + jest-util: 30.3.0 + p-limit: 3.1.0 + + jest-circus@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + p-limit: 3.1.0 + pretty-format: 30.3.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.3.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + parse-json: 5.2.0 + pretty-format: 30.3.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.15 + ts-node: 10.9.2(@types/node@22.19.15)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + + jest-docblock@30.2.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + jest-util: 30.3.0 + pretty-format: 30.3.0 + + jest-environment-node@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + + jest-haste-map@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: 4.0.3 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.3.0 + + jest-matcher-utils@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + + jest-message-util@30.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + jest-util: 30.3.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): + optionalDependencies: + jest-resolve: 30.3.0 + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.3.0: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.3.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) + jest-util: 30.3.0 + jest-validate: 30.3.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + + jest-runner@30.3.0: + dependencies: + '@jest/console': 30.3.0 + '@jest/environment': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/globals': 30.3.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.3.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 30.3.0 + graceful-fs: 4.2.11 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 + semver: 7.7.4 + synckit: 0.11.12 + transitivePeerDependencies: + - supports-color + + jest-util@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + jest-validate@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.3.0 + + jest-watcher@30.3.0: + dependencies: + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.3.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 22.19.15 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@30.3.0: + dependencies: + '@types/node': 22.19.15 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.3.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@jest/types': 30.3.0 + import-local: 3.2.0 + jest-cli: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + load-esm@1.0.3: {} + + loader-runner@4.3.1: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.23: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + lru-cache@11.2.7: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mute-stream@2.0.0: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + node-abort-controller@3.1.1: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.17.23 + + node-int64@0.4.0: {} + + node-releases@2.0.36: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + path-to-regexp@8.3.0: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pluralize@8.0.0: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.8.1: {} + + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + pure-rand@7.0.1: {} + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-is@18.3.1: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + socket.io-adapter@2.5.6: + dependencies: + debug: 4.4.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.6: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.6 + debug: 4.4.3 + engine.io: 6.6.6 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + source-map@0.7.6: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + statuses@2.0.2: {} + + streamsearch@1.1.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.0 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + symbol-observable@4.0.0: {} + + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + tapable@2.3.0: {} + + terser-webpack-plugin@5.4.0(webpack@5.104.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.104.1 + + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.5 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + jest-util: 30.3.0 + + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.20.1 + micromatch: 4.0.8 + semver: 7.7.4 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.104.1 + + ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.15 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.20.1 + tapable: 2.3.0 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript-eslint@8.57.1(eslint@9.39.4)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vary@1.1.2: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.3.4: {} + + webpack@5.104.1: + 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.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.4.0(webpack@5.104.1) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@8.18.3: {} + + ws@8.20.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod@4.3.6: {} diff --git a/src/ai/ai-chat.controller.ts b/src/ai/ai-chat.controller.ts index 0429708..39bc96b 100644 --- a/src/ai/ai-chat.controller.ts +++ b/src/ai/ai-chat.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Post, Body, Headers, HttpException, Logger } from '@nestjs/common'; +import { + Controller, + Post, + Body, + Headers, + HttpException, + Logger, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { generateText, tool, stepCountIs } from 'ai'; import type { LanguageModel } from 'ai'; @@ -7,72 +14,83 @@ import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { createAiModel, isAiConfigured } from './ai-provider'; type ChatRequest = { - message: string; - context?: { callerPhone?: string; leadId?: string; leadName?: string }; + message: string; + context?: { callerPhone?: string; leadId?: string; leadName?: string }; }; @Controller('api/ai') export class AiChatController { - private readonly logger = new Logger(AiChatController.name); - private readonly aiModel: LanguageModel | null; - private knowledgeBase: string | null = null; - private kbLoadedAt = 0; - private readonly kbTtlMs = 5 * 60 * 1000; + private readonly logger = new Logger(AiChatController.name); + private readonly aiModel: LanguageModel | null; + private knowledgeBase: string | null = null; + private kbLoadedAt = 0; + private readonly kbTtlMs = 5 * 60 * 1000; - constructor( - private config: ConfigService, - private platform: PlatformGraphqlService, - ) { - this.aiModel = createAiModel(config); - if (!this.aiModel) { - this.logger.warn('AI not configured — chat uses fallback'); - } else { - const provider = config.get('ai.provider') ?? 'openai'; - const model = config.get('ai.model') ?? 'gpt-4o-mini'; - this.logger.log(`AI configured: ${provider}/${model}`); - } + constructor( + private config: ConfigService, + private platform: PlatformGraphqlService, + ) { + this.aiModel = createAiModel(config); + if (!this.aiModel) { + this.logger.warn('AI not configured — chat uses fallback'); + } else { + const provider = config.get('ai.provider') ?? 'openai'; + const model = config.get('ai.model') ?? 'gpt-4o-mini'; + this.logger.log(`AI configured: ${provider}/${model}`); + } + } + + @Post('chat') + async chat( + @Body() body: ChatRequest, + @Headers('authorization') auth: string, + ) { + if (!auth) throw new HttpException('Authorization required', 401); + if (!body.message?.trim()) throw new HttpException('message required', 400); + + const msg = body.message.trim(); + const ctx = body.context; + let prefix = ''; + if (ctx) { + const parts: string[] = []; + if (ctx.leadName) parts.push(`Caller: ${ctx.leadName}`); + if (ctx.callerPhone) parts.push(`Phone: ${ctx.callerPhone}`); + if (ctx.leadId) parts.push(`Lead ID: ${ctx.leadId}`); + if (parts.length) prefix = `[Active call: ${parts.join(', ')}]\n\n`; } - @Post('chat') - async chat(@Body() body: ChatRequest, @Headers('authorization') auth: string) { - if (!auth) throw new HttpException('Authorization required', 401); - if (!body.message?.trim()) throw new HttpException('message required', 400); - - const msg = body.message.trim(); - const ctx = body.context; - let prefix = ''; - if (ctx) { - const parts: string[] = []; - if (ctx.leadName) parts.push(`Caller: ${ctx.leadName}`); - if (ctx.callerPhone) parts.push(`Phone: ${ctx.callerPhone}`); - if (ctx.leadId) parts.push(`Lead ID: ${ctx.leadId}`); - if (parts.length) prefix = `[Active call: ${parts.join(', ')}]\n\n`; - } - - if (!this.aiModel) { - return { reply: await this.fallback(msg, auth), sources: ['fallback'], confidence: 'low' }; - } - - try { - return await this.chatWithTools(`${prefix}${msg}`, auth); - } catch (err) { - this.logger.error(`AI chat error: ${err}`); - return { reply: await this.fallback(msg, auth), sources: ['fallback'], confidence: 'low' }; - } + if (!this.aiModel) { + return { + reply: await this.fallback(msg, auth), + sources: ['fallback'], + confidence: 'low', + }; } - private async buildKnowledgeBase(auth: string): Promise { - const now = Date.now(); - if (this.knowledgeBase && now - this.kbLoadedAt < this.kbTtlMs) { - return this.knowledgeBase; - } + try { + return await this.chatWithTools(`${prefix}${msg}`, auth); + } catch (err) { + this.logger.error(`AI chat error: ${err}`); + return { + reply: await this.fallback(msg, auth), + sources: ['fallback'], + confidence: 'low', + }; + } + } - this.logger.log('Building knowledge base from platform data...'); - const sections: string[] = []; + private async buildKnowledgeBase(auth: string): Promise { + const now = Date.now(); + if (this.knowledgeBase && now - this.kbLoadedAt < this.kbTtlMs) { + return this.knowledgeBase; + } - try { - const clinicData = await this.platform.queryWithAuth( - `{ clinics(first: 20) { edges { node { + this.logger.log('Building knowledge base from platform data...'); + const sections: string[] = []; + + try { + const clinicData = await this.platform.queryWithAuth( + `{ clinics(first: 20) { edges { node { id name clinicName addressCustom { addressStreet1 addressCity addressState addressPostcode } weekdayHours saturdayHours sundayHours @@ -80,111 +98,132 @@ export class AiChatController { cancellationWindowHours arriveEarlyMin requiredDocuments acceptsCash acceptsCard acceptsUpi } } } }`, - undefined, auth, - ); - const clinics = clinicData.clinics.edges.map((e: any) => e.node); - if (clinics.length) { - sections.push('## Clinics'); - for (const c of clinics) { - const addr = c.addressCustom - ? [c.addressCustom.addressStreet1, c.addressCustom.addressCity].filter(Boolean).join(', ') - : ''; - const hours = [ - c.weekdayHours ? `Mon–Fri ${c.weekdayHours}` : '', - c.saturdayHours ? `Sat ${c.saturdayHours}` : '', - c.sundayHours ? `Sun ${c.sundayHours}` : 'Sun closed', - ].filter(Boolean).join(', '); - sections.push(`- ${c.clinicName ?? c.name}: ${addr}. ${hours}.`); - } - - const rulesClinic = clinics[0]; - const rules: string[] = []; - if (rulesClinic.cancellationWindowHours) rules.push(`Free cancellation up to ${rulesClinic.cancellationWindowHours}h before`); - if (rulesClinic.arriveEarlyMin) rules.push(`Arrive ${rulesClinic.arriveEarlyMin}min early`); - if (rulesClinic.requiredDocuments) rules.push(`First-time patients bring ${rulesClinic.requiredDocuments}`); - if (rulesClinic.walkInAllowed) rules.push('Walk-ins accepted'); - if (rulesClinic.onlineBooking) rules.push('Online booking available'); - if (rules.length) { - sections.push('\n### Booking Rules'); - sections.push(rules.map(r => `- ${r}`).join('\n')); - } - - const payments: string[] = []; - if (rulesClinic.acceptsCash === 'YES') payments.push('Cash'); - if (rulesClinic.acceptsCard === 'YES') payments.push('Cards'); - if (rulesClinic.acceptsUpi === 'YES') payments.push('UPI'); - if (payments.length) { - sections.push('\n### Payments'); - sections.push(`Accepted: ${payments.join(', ')}.`); - } - } - } catch (err) { - this.logger.warn(`Failed to fetch clinics: ${err}`); + undefined, + auth, + ); + const clinics = clinicData.clinics.edges.map((e: any) => e.node); + if (clinics.length) { + sections.push('## Clinics'); + for (const c of clinics) { + const addr = c.addressCustom + ? [c.addressCustom.addressStreet1, c.addressCustom.addressCity] + .filter(Boolean) + .join(', ') + : ''; + const hours = [ + c.weekdayHours ? `Mon–Fri ${c.weekdayHours}` : '', + c.saturdayHours ? `Sat ${c.saturdayHours}` : '', + c.sundayHours ? `Sun ${c.sundayHours}` : 'Sun closed', + ] + .filter(Boolean) + .join(', '); + sections.push(`- ${c.clinicName ?? c.name}: ${addr}. ${hours}.`); } - try { - const pkgData = await this.platform.queryWithAuth( - `{ healthPackages(first: 30, filter: { active: { eq: true } }) { edges { node { + const rulesClinic = clinics[0]; + const rules: string[] = []; + if (rulesClinic.cancellationWindowHours) + rules.push( + `Free cancellation up to ${rulesClinic.cancellationWindowHours}h before`, + ); + if (rulesClinic.arriveEarlyMin) + rules.push(`Arrive ${rulesClinic.arriveEarlyMin}min early`); + if (rulesClinic.requiredDocuments) + rules.push( + `First-time patients bring ${rulesClinic.requiredDocuments}`, + ); + if (rulesClinic.walkInAllowed) rules.push('Walk-ins accepted'); + if (rulesClinic.onlineBooking) rules.push('Online booking available'); + if (rules.length) { + sections.push('\n### Booking Rules'); + sections.push(rules.map((r) => `- ${r}`).join('\n')); + } + + const payments: string[] = []; + if (rulesClinic.acceptsCash === 'YES') payments.push('Cash'); + if (rulesClinic.acceptsCard === 'YES') payments.push('Cards'); + if (rulesClinic.acceptsUpi === 'YES') payments.push('UPI'); + if (payments.length) { + sections.push('\n### Payments'); + sections.push(`Accepted: ${payments.join(', ')}.`); + } + } + } catch (err) { + this.logger.warn(`Failed to fetch clinics: ${err}`); + } + + try { + const pkgData = await this.platform.queryWithAuth( + `{ healthPackages(first: 30, filter: { active: { eq: true } }) { edges { node { id name packageName description price { amountMicros currencyCode } discountedPrice { amountMicros currencyCode } department inclusions durationMin eligibility packageTests { edges { node { labTest { testName category } order } } } } } } }`, - undefined, auth, - ); - const packages = pkgData.healthPackages.edges.map((e: any) => e.node); - if (packages.length) { - sections.push('\n## Health Packages'); - for (const p of packages) { - const price = p.price ? `₹${p.price.amountMicros / 1_000_000}` : ''; - const disc = p.discountedPrice?.amountMicros ? ` (discounted: ₹${p.discountedPrice.amountMicros / 1_000_000})` : ''; - const dept = p.department ? ` [${p.department}]` : ''; - sections.push(`- ${p.packageName ?? p.name}: ${price}${disc}${dept}`); - const tests = p.packageTests?.edges - ?.map((e: any) => e.node) - ?.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0)) - ?.map((t: any) => t.labTest?.testName) - ?.filter(Boolean); - if (tests?.length) { - sections.push(` Tests: ${tests.join(', ')}`); - } else if (p.inclusions) { - sections.push(` Includes: ${p.inclusions}`); - } - } - } - } catch (err) { - this.logger.warn(`Failed to fetch health packages: ${err}`); + undefined, + auth, + ); + const packages = pkgData.healthPackages.edges.map((e: any) => e.node); + if (packages.length) { + sections.push('\n## Health Packages'); + for (const p of packages) { + const price = p.price ? `₹${p.price.amountMicros / 1_000_000}` : ''; + const disc = p.discountedPrice?.amountMicros + ? ` (discounted: ₹${p.discountedPrice.amountMicros / 1_000_000})` + : ''; + const dept = p.department ? ` [${p.department}]` : ''; + sections.push(`- ${p.packageName ?? p.name}: ${price}${disc}${dept}`); + const tests = p.packageTests?.edges + ?.map((e: any) => e.node) + ?.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0)) + ?.map((t: any) => t.labTest?.testName) + ?.filter(Boolean); + if (tests?.length) { + sections.push(` Tests: ${tests.join(', ')}`); + } else if (p.inclusions) { + sections.push(` Includes: ${p.inclusions}`); + } } - - try { - const insData = await this.platform.queryWithAuth( - `{ insurancePartners(first: 30, filter: { empanelmentStatus: { eq: ACTIVE } }) { edges { node { - id name insurerName tpaName settlementType planTypesAccepted - } } } }`, - undefined, auth, - ); - const insurers = insData.insurancePartners.edges.map((e: any) => e.node); - if (insurers.length) { - sections.push('\n## Insurance Partners'); - const names = insurers.map((i: any) => { - const settlement = i.settlementType ? ` (${i.settlementType.toLowerCase()})` : ''; - return `${i.insurerName ?? i.name}${settlement}`; - }); - sections.push(names.join(', ')); - } - } catch (err) { - this.logger.warn(`Failed to fetch insurance partners: ${err}`); - } - - this.knowledgeBase = sections.join('\n') || 'No hospital information available yet.'; - this.kbLoadedAt = now; - this.logger.log(`Knowledge base built (${this.knowledgeBase.length} chars)`); - return this.knowledgeBase; + } + } catch (err) { + this.logger.warn(`Failed to fetch health packages: ${err}`); } - private buildSystemPrompt(kb: string): string { - return `You are an AI assistant for call center agents at a hospital. + try { + const insData = await this.platform.queryWithAuth( + `{ insurancePartners(first: 30, filter: { empanelmentStatus: { eq: ACTIVE } }) { edges { node { + id name insurerName tpaName settlementType planTypesAccepted + } } } }`, + undefined, + auth, + ); + const insurers = insData.insurancePartners.edges.map((e: any) => e.node); + if (insurers.length) { + sections.push('\n## Insurance Partners'); + const names = insurers.map((i: any) => { + const settlement = i.settlementType + ? ` (${i.settlementType.toLowerCase()})` + : ''; + return `${i.insurerName ?? i.name}${settlement}`; + }); + sections.push(names.join(', ')); + } + } catch (err) { + this.logger.warn(`Failed to fetch insurance partners: ${err}`); + } + + this.knowledgeBase = + sections.join('\n') || 'No hospital information available yet.'; + this.kbLoadedAt = now; + this.logger.log( + `Knowledge base built (${this.knowledgeBase.length} chars)`, + ); + return this.knowledgeBase; + } + + private buildSystemPrompt(kb: string): string { + return `You are an AI assistant for call center agents at a hospital. You help agents answer questions about patients, doctors, appointments, and hospital services during live calls. RULES: @@ -198,28 +237,29 @@ RULES: 8. Format with bullet points for easy scanning. ${kb}`; - } + } - private async chatWithTools(userMessage: string, auth: string) { - const kb = await this.buildKnowledgeBase(auth); - const systemPrompt = this.buildSystemPrompt(kb); - const platformService = this.platform; + private async chatWithTools(userMessage: string, auth: string) { + const kb = await this.buildKnowledgeBase(auth); + const systemPrompt = this.buildSystemPrompt(kb); + const platformService = this.platform; - const { text, steps } = await generateText({ - model: this.aiModel!, - system: systemPrompt, - prompt: userMessage, - stopWhen: stepCountIs(5), - tools: { - lookup_patient: tool({ - description: 'Search for a patient/lead by phone number or name. Returns their profile, lead status, AI summary, and linked patient/campaign IDs.', - inputSchema: z.object({ - phone: z.string().optional().describe('Phone number to search'), - name: z.string().optional().describe('Patient/lead name to search'), - }), - execute: async ({ phone, name }) => { - const data = await platformService.queryWithAuth( - `{ leads(first: 50) { edges { node { + const { text, steps } = await generateText({ + model: this.aiModel!, + system: systemPrompt, + prompt: userMessage, + stopWhen: stepCountIs(5), + tools: { + lookup_patient: tool({ + description: + 'Search for a patient/lead by phone number or name. Returns their profile, lead status, AI summary, and linked patient/campaign IDs.', + inputSchema: z.object({ + phone: z.string().optional().describe('Phone number to search'), + name: z.string().optional().describe('Patient/lead name to search'), + }), + execute: async ({ phone, name }) => { + const data = await platformService.queryWithAuth( + `{ leads(first: 50) { edges { node { id name contactName { firstName lastName } contactPhone { primaryPhoneNumber } contactEmail { primaryEmail } @@ -227,86 +267,106 @@ ${kb}`; leadScore contactAttempts firstContacted lastContacted aiSummary aiSuggestedAction patientId campaignId } } } }`, - undefined, auth, - ); - const leads = data.leads.edges.map((e: any) => e.node); - const phoneClean = (phone ?? '').replace(/\D/g, ''); - const nameClean = (name ?? '').toLowerCase(); + undefined, + auth, + ); + const leads = data.leads.edges.map((e: any) => e.node); + const phoneClean = (phone ?? '').replace(/\D/g, ''); + const nameClean = (name ?? '').toLowerCase(); - const matched = leads.filter((l: any) => { - if (phoneClean) { - const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, ''); - if (lp.endsWith(phoneClean) || phoneClean.endsWith(lp)) return true; - } - if (nameClean) { - const fn = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); - if (fn.includes(nameClean)) return true; - } - return false; - }); + const matched = leads.filter((l: any) => { + if (phoneClean) { + const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace( + /\D/g, + '', + ); + if (lp.endsWith(phoneClean) || phoneClean.endsWith(lp)) + return true; + } + if (nameClean) { + const fn = + `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); + if (fn.includes(nameClean)) return true; + } + return false; + }); - if (!matched.length) return { found: false, message: 'No patient/lead found.' }; - return { found: true, count: matched.length, leads: matched }; - }, - }), + if (!matched.length) + return { found: false, message: 'No patient/lead found.' }; + return { found: true, count: matched.length, leads: matched }; + }, + }), - lookup_appointments: tool({ - description: 'Get all appointments (past and upcoming) for a patient. Returns doctor, department, date, status, and reason.', - inputSchema: z.object({ - patientId: z.string().describe('Patient ID'), - }), - execute: async ({ patientId }) => { - const data = await platformService.queryWithAuth( - `{ appointments(first: 20, filter: { patientId: { eq: "${patientId}" } }, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { + lookup_appointments: tool({ + description: + 'Get all appointments (past and upcoming) for a patient. Returns doctor, department, date, status, and reason.', + inputSchema: z.object({ + patientId: z.string().describe('Patient ID'), + }), + execute: async ({ patientId }) => { + const data = await platformService.queryWithAuth( + `{ appointments(first: 20, filter: { patientId: { eq: "${patientId}" } }, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { id name scheduledAt durationMin appointmentType status doctorName department reasonForVisit doctorId } } } }`, - undefined, auth, - ); - return { appointments: data.appointments.edges.map((e: any) => e.node) }; - }, - }), + undefined, + auth, + ); + return { + appointments: data.appointments.edges.map((e: any) => e.node), + }; + }, + }), - lookup_call_history: tool({ - description: 'Get call log for a lead — all inbound/outbound calls with dispositions and durations.', - inputSchema: z.object({ - leadId: z.string().describe('Lead ID'), - }), - execute: async ({ leadId }) => { - const data = await platformService.queryWithAuth( - `{ calls(first: 20, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { + lookup_call_history: tool({ + description: + 'Get call log for a lead — all inbound/outbound calls with dispositions and durations.', + inputSchema: z.object({ + leadId: z.string().describe('Lead ID'), + }), + execute: async ({ leadId }) => { + const data = await platformService.queryWithAuth( + `{ calls(first: 20, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id name direction callStatus agentName startedAt durationSec disposition } } } }`, - undefined, auth, - ); - return { calls: data.calls.edges.map((e: any) => e.node) }; - }, - }), + undefined, + auth, + ); + return { calls: data.calls.edges.map((e: any) => e.node) }; + }, + }), - lookup_lead_activities: tool({ - description: 'Get the full interaction timeline — status changes, calls, WhatsApp, notes, appointments.', - inputSchema: z.object({ - leadId: z.string().describe('Lead ID'), - }), - execute: async ({ leadId }) => { - const data = await platformService.queryWithAuth( - `{ leadActivities(first: 30, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node { + lookup_lead_activities: tool({ + description: + 'Get the full interaction timeline — status changes, calls, WhatsApp, notes, appointments.', + inputSchema: z.object({ + leadId: z.string().describe('Lead ID'), + }), + execute: async ({ leadId }) => { + const data = await platformService.queryWithAuth( + `{ leadActivities(first: 30, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node { id activityType summary occurredAt performedBy channel } } } }`, - undefined, auth, - ); - return { activities: data.leadActivities.edges.map((e: any) => e.node) }; - }, - }), + undefined, + auth, + ); + return { + activities: data.leadActivities.edges.map((e: any) => e.node), + }; + }, + }), - lookup_doctor: tool({ - description: 'Get doctor details — schedule, clinic, fees, qualifications, specialty. Search by name.', - inputSchema: z.object({ - doctorName: z.string().describe('Doctor name (e.g. "Patel", "Sharma")'), - }), - execute: async ({ doctorName }) => { - const data = await platformService.queryWithAuth( - `{ doctors(first: 10) { edges { node { + lookup_doctor: tool({ + description: + 'Get doctor details — schedule, clinic, fees, qualifications, specialty. Search by name.', + inputSchema: z.object({ + doctorName: z + .string() + .describe('Doctor name (e.g. "Patel", "Sharma")'), + }), + execute: async ({ doctorName }) => { + const data = await platformService.queryWithAuth( + `{ doctors(first: 10) { edges { node { id name fullName { firstName lastName } department specialty qualifications yearsOfExperience visitingHours @@ -315,87 +375,125 @@ ${kb}`; active registrationNumber clinic { id name clinicName } } } } }`, - undefined, auth, - ); + undefined, + auth, + ); - const doctors = data.doctors.edges.map((e: any) => e.node); - const search = doctorName.toLowerCase(); - const matched = doctors.filter((d: any) => { - const full = `${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''} ${d.name ?? ''}`.toLowerCase(); - return full.includes(search); - }); + const doctors = data.doctors.edges.map((e: any) => e.node); + const search = doctorName.toLowerCase(); + const matched = doctors.filter((d: any) => { + const full = + `${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''} ${d.name ?? ''}`.toLowerCase(); + return full.includes(search); + }); - if (!matched.length) return { found: false, message: `No doctor matching "${doctorName}"` }; + if (!matched.length) + return { + found: false, + message: `No doctor matching "${doctorName}"`, + }; - return { - found: true, - doctors: matched.map((d: any) => ({ - ...d, - clinicName: d.clinic?.clinicName ?? d.clinic?.name ?? 'N/A', - feeNewFormatted: d.consultationFeeNew ? `₹${d.consultationFeeNew.amountMicros / 1_000_000}` : 'N/A', - feeFollowUpFormatted: d.consultationFeeFollowUp ? `₹${d.consultationFeeFollowUp.amountMicros / 1_000_000}` : 'N/A', - })), - }; - }, - }), - }, - }); + return { + found: true, + doctors: matched.map((d: any) => ({ + ...d, + clinicName: d.clinic?.clinicName ?? d.clinic?.name ?? 'N/A', + feeNewFormatted: d.consultationFeeNew + ? `₹${d.consultationFeeNew.amountMicros / 1_000_000}` + : 'N/A', + feeFollowUpFormatted: d.consultationFeeFollowUp + ? `₹${d.consultationFeeFollowUp.amountMicros / 1_000_000}` + : 'N/A', + })), + }; + }, + }), + }, + }); - const toolCallCount = steps.filter(s => s.toolCalls?.length).length; - this.logger.log(`Response (${text.length} chars, ${toolCallCount} tool steps)`); + const toolCallCount = steps.filter((s) => s.toolCalls?.length).length; + this.logger.log( + `Response (${text.length} chars, ${toolCallCount} tool steps)`, + ); - return { - reply: text, - sources: toolCallCount > 0 ? ['platform_db', 'hospital_kb'] : ['hospital_kb'], - confidence: 'high', - }; - } + return { + reply: text, + sources: + toolCallCount > 0 ? ['platform_db', 'hospital_kb'] : ['hospital_kb'], + confidence: 'high', + }; + } - private async fallback(msg: string, auth: string): Promise { - try { - const doctors = await this.platform.queryWithAuth( - `{ doctors(first: 10) { edges { node { + private async fallback(msg: string, auth: string): Promise { + try { + const doctors = await this.platform.queryWithAuth( + `{ doctors(first: 10) { edges { node { name fullName { firstName lastName } department specialty visitingHours consultationFeeNew { amountMicros currencyCode } clinic { name clinicName } } } } }`, - undefined, auth, - ); - const docs = doctors.doctors.edges.map((e: any) => e.node); - const l = msg.toLowerCase(); + undefined, + auth, + ); + const docs = doctors.doctors.edges.map((e: any) => e.node); + const l = msg.toLowerCase(); - const matchedDoc = docs.find((d: any) => { - const full = `${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''} ${d.name ?? ''}`.toLowerCase(); - return l.split(/\s+/).some((w: string) => w.length > 2 && full.includes(w)); - }); - if (matchedDoc) { - const fee = matchedDoc.consultationFeeNew ? `₹${matchedDoc.consultationFeeNew.amountMicros / 1_000_000}` : ''; - const clinic = matchedDoc.clinic?.clinicName ?? ''; - return `Dr. ${matchedDoc.fullName?.lastName ?? matchedDoc.name} (${matchedDoc.department ?? matchedDoc.specialty}): ${matchedDoc.visitingHours ?? 'hours not set'}${clinic ? ` at ${clinic}` : ''}${fee ? `. Fee: ${fee}` : ''}.`; - } + const matchedDoc = docs.find((d: any) => { + const full = + `${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''} ${d.name ?? ''}`.toLowerCase(); + return l + .split(/\s+/) + .some((w: string) => w.length > 2 && full.includes(w)); + }); + if (matchedDoc) { + const fee = matchedDoc.consultationFeeNew + ? `₹${matchedDoc.consultationFeeNew.amountMicros / 1_000_000}` + : ''; + const clinic = matchedDoc.clinic?.clinicName ?? ''; + return `Dr. ${matchedDoc.fullName?.lastName ?? matchedDoc.name} (${matchedDoc.department ?? matchedDoc.specialty}): ${matchedDoc.visitingHours ?? 'hours not set'}${clinic ? ` at ${clinic}` : ''}${fee ? `. Fee: ${fee}` : ''}.`; + } - if (l.includes('doctor') || l.includes('available')) { - return 'Doctors: ' + docs.map((d: any) => - `${d.fullName?.lastName ?? d.name} (${d.department ?? d.specialty})` - ).join(', ') + '.'; - } + if (l.includes('doctor') || l.includes('available')) { + return ( + 'Doctors: ' + + docs + .map( + (d: any) => + `${d.fullName?.lastName ?? d.name} (${d.department ?? d.specialty})`, + ) + .join(', ') + + '.' + ); + } - if (l.includes('package') || l.includes('checkup') || l.includes('screening')) { - const pkgs = await this.platform.queryWithAuth( - `{ healthPackages(first: 20) { edges { node { packageName price { amountMicros } } } } }`, - undefined, auth, - ); - const packages = pkgs.healthPackages.edges.map((e: any) => e.node); - if (packages.length) { - return 'Packages: ' + packages.map((p: any) => - `${p.packageName} ₹${p.price?.amountMicros ? p.price.amountMicros / 1_000_000 : 'N/A'}` - ).join(' | ') + '.'; - } - } - } catch { - // platform unreachable + if ( + l.includes('package') || + l.includes('checkup') || + l.includes('screening') + ) { + const pkgs = await this.platform.queryWithAuth( + `{ healthPackages(first: 20) { edges { node { packageName price { amountMicros } } } } }`, + undefined, + auth, + ); + const packages = pkgs.healthPackages.edges.map((e: any) => e.node); + if (packages.length) { + return ( + 'Packages: ' + + packages + .map( + (p: any) => + `${p.packageName} ₹${p.price?.amountMicros ? p.price.amountMicros / 1_000_000 : 'N/A'}`, + ) + .join(' | ') + + '.' + ); } - - return 'I can help with: doctor schedules, patient lookup, appointments, packages, insurance. What do you need?'; + } + } catch { + // platform unreachable } + + return 'I can help with: doctor schedules, patient lookup, appointments, packages, insurance. What do you need?'; + } } diff --git a/src/ai/ai-enrichment.service.ts b/src/ai/ai-enrichment.service.ts index 389daae..95e3fe0 100644 --- a/src/ai/ai-enrichment.service.ts +++ b/src/ai/ai-enrichment.service.ts @@ -6,57 +6,66 @@ import { z } from 'zod'; import { createAiModel } from './ai-provider'; type LeadContext = { - firstName?: string; - lastName?: string; - leadSource?: string; - interestedService?: string; - leadStatus?: string; - contactAttempts?: number; - createdAt?: string; - campaignId?: string; - activities?: { activityType: string; summary: string }[]; + firstName?: string; + lastName?: string; + leadSource?: string; + interestedService?: string; + leadStatus?: string; + contactAttempts?: number; + createdAt?: string; + campaignId?: string; + activities?: { activityType: string; summary: string }[]; }; type EnrichmentResult = { - aiSummary: string; - aiSuggestedAction: string; + aiSummary: string; + aiSuggestedAction: string; }; const enrichmentSchema = z.object({ - aiSummary: z.string().describe('1-2 sentence summary of who this lead is and their history'), - aiSuggestedAction: z.string().describe('5-10 word suggested action for the agent'), + aiSummary: z + .string() + .describe('1-2 sentence summary of who this lead is and their history'), + aiSuggestedAction: z + .string() + .describe('5-10 word suggested action for the agent'), }); @Injectable() export class AiEnrichmentService { - private readonly logger = new Logger(AiEnrichmentService.name); - private readonly aiModel: LanguageModel | null; + private readonly logger = new Logger(AiEnrichmentService.name); + private readonly aiModel: LanguageModel | null; - constructor(private config: ConfigService) { - this.aiModel = createAiModel(config); - if (!this.aiModel) { - this.logger.warn('AI not configured — enrichment uses fallback'); - } + constructor(private config: ConfigService) { + this.aiModel = createAiModel(config); + if (!this.aiModel) { + this.logger.warn('AI not configured — enrichment uses fallback'); + } + } + + async enrichLead(lead: LeadContext): Promise { + if (!this.aiModel) { + return this.fallbackEnrichment(lead); } - async enrichLead(lead: LeadContext): Promise { - if (!this.aiModel) { - return this.fallbackEnrichment(lead); - } + try { + const daysSince = lead.createdAt + ? Math.floor( + (Date.now() - new Date(lead.createdAt).getTime()) / + (1000 * 60 * 60 * 24), + ) + : 0; - try { - const daysSince = lead.createdAt - ? Math.floor((Date.now() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24)) - : 0; + const activitiesText = lead.activities?.length + ? lead.activities + .map((a) => `- ${a.activityType}: ${a.summary}`) + .join('\n') + : 'No previous interactions'; - const activitiesText = lead.activities?.length - ? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n') - : 'No previous interactions'; - - const { object } = await generateObject({ - model: this.aiModel!, - schema: enrichmentSchema, - prompt: `You are an AI assistant for a hospital call center. + const { object } = await generateObject({ + model: this.aiModel, + schema: enrichmentSchema, + prompt: `You are an AI assistant for a hospital call center. An inbound call is coming in from a lead. Summarize their history and suggest what the call center agent should do. Lead details: @@ -69,39 +78,45 @@ Lead details: Recent activity: ${activitiesText}`, - }); + }); - this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`); - return object; - } catch (error) { - this.logger.error(`AI enrichment failed: ${error}`); - return this.fallbackEnrichment(lead); - } + this.logger.log( + `AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`, + ); + return object; + } catch (error) { + this.logger.error(`AI enrichment failed: ${error}`); + return this.fallbackEnrichment(lead); + } + } + + private fallbackEnrichment(lead: LeadContext): EnrichmentResult { + const daysSince = lead.createdAt + ? Math.floor( + (Date.now() - new Date(lead.createdAt).getTime()) / + (1000 * 60 * 60 * 24), + ) + : 0; + + const attempts = lead.contactAttempts ?? 0; + const service = lead.interestedService ?? 'general inquiry'; + const source = + lead.leadSource?.replace(/_/g, ' ').toLowerCase() ?? 'unknown source'; + + let summary: string; + let action: string; + + if (attempts === 0) { + summary = `First-time inquiry from ${source}. Interested in ${service}. Lead is ${daysSince} day(s) old with no previous contact.`; + action = `Introduce services and offer appointment booking`; + } else if (attempts === 1) { + summary = `Returning inquiry — contacted once before. Interested in ${service} via ${source}. Lead is ${daysSince} day(s) old.`; + action = `Follow up on previous conversation, offer appointment`; + } else { + summary = `Repeat contact (${attempts} previous attempts) for ${service}. Originally from ${source}, ${daysSince} day(s) old. Shows high interest.`; + action = `Prioritize appointment booking — high-intent lead`; } - private fallbackEnrichment(lead: LeadContext): EnrichmentResult { - const daysSince = lead.createdAt - ? Math.floor((Date.now() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24)) - : 0; - - const attempts = lead.contactAttempts ?? 0; - const service = lead.interestedService ?? 'general inquiry'; - const source = lead.leadSource?.replace(/_/g, ' ').toLowerCase() ?? 'unknown source'; - - let summary: string; - let action: string; - - if (attempts === 0) { - summary = `First-time inquiry from ${source}. Interested in ${service}. Lead is ${daysSince} day(s) old with no previous contact.`; - action = `Introduce services and offer appointment booking`; - } else if (attempts === 1) { - summary = `Returning inquiry — contacted once before. Interested in ${service} via ${source}. Lead is ${daysSince} day(s) old.`; - action = `Follow up on previous conversation, offer appointment`; - } else { - summary = `Repeat contact (${attempts} previous attempts) for ${service}. Originally from ${source}, ${daysSince} day(s) old. Shows high interest.`; - action = `Prioritize appointment booking — high-intent lead`; - } - - return { aiSummary: summary, aiSuggestedAction: action }; - } + return { aiSummary: summary, aiSuggestedAction: action }; + } } diff --git a/src/ai/ai-provider.ts b/src/ai/ai-provider.ts index bfd2fc2..4f09337 100644 --- a/src/ai/ai-provider.ts +++ b/src/ai/ai-provider.ts @@ -4,23 +4,24 @@ import { openai } from '@ai-sdk/openai'; import type { LanguageModel } from 'ai'; export function createAiModel(config: ConfigService): LanguageModel | null { - const provider = config.get('ai.provider') ?? 'openai'; - const model = config.get('ai.model') ?? 'gpt-4o-mini'; + const provider = config.get('ai.provider') ?? 'openai'; + const model = config.get('ai.model') ?? 'gpt-4o-mini'; - if (provider === 'anthropic') { - const apiKey = config.get('ai.anthropicApiKey'); - if (!apiKey) return null; - return anthropic(model); - } - - // Default to openai - const apiKey = config.get('ai.openaiApiKey'); + if (provider === 'anthropic') { + const apiKey = config.get('ai.anthropicApiKey'); if (!apiKey) return null; - return openai(model); + return anthropic(model); + } + + // Default to openai + const apiKey = config.get('ai.openaiApiKey'); + if (!apiKey) return null; + return openai(model); } export function isAiConfigured(config: ConfigService): boolean { - const provider = config.get('ai.provider') ?? 'openai'; - if (provider === 'anthropic') return !!config.get('ai.anthropicApiKey'); - return !!config.get('ai.openaiApiKey'); + const provider = config.get('ai.provider') ?? 'openai'; + if (provider === 'anthropic') + return !!config.get('ai.anthropicApiKey'); + return !!config.get('ai.openaiApiKey'); } diff --git a/src/ai/ai.module.ts b/src/ai/ai.module.ts index 27d2a80..2db6a2c 100644 --- a/src/ai/ai.module.ts +++ b/src/ai/ai.module.ts @@ -4,9 +4,9 @@ import { AiEnrichmentService } from './ai-enrichment.service'; import { AiChatController } from './ai-chat.controller'; @Module({ - imports: [PlatformModule], - controllers: [AiChatController], - providers: [AiEnrichmentService], - exports: [AiEnrichmentService], + imports: [PlatformModule], + controllers: [AiChatController], + providers: [AiEnrichmentService], + exports: [AiEnrichmentService], }) export class AiModule {} diff --git a/src/app.module.ts b/src/app.module.ts index 5c55f4a..da985ef 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,22 +14,22 @@ import { CallAssistModule } from './call-assist/call-assist.module'; import { SearchModule } from './search/search.module'; @Module({ - imports: [ - ConfigModule.forRoot({ - load: [configuration], - isGlobal: true, - }), - AiModule, - AuthModule, - PlatformModule, - ExotelModule, - CallEventsModule, - OzonetelAgentModule, - GraphqlProxyModule, - HealthModule, - WorklistModule, - CallAssistModule, - SearchModule, - ], + imports: [ + ConfigModule.forRoot({ + load: [configuration], + isGlobal: true, + }), + AiModule, + AuthModule, + PlatformModule, + ExotelModule, + CallEventsModule, + OzonetelAgentModule, + GraphqlProxyModule, + HealthModule, + WorklistModule, + CallAssistModule, + SearchModule, + ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index bce2340..559d932 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -5,28 +5,32 @@ import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; @Controller('auth') export class AuthController { - private readonly logger = new Logger(AuthController.name); - private readonly graphqlUrl: string; - private readonly workspaceSubdomain: string; - private readonly origin: string; + private readonly logger = new Logger(AuthController.name); + private readonly graphqlUrl: string; + private readonly workspaceSubdomain: string; + private readonly origin: string; - constructor( - private config: ConfigService, - private ozonetelAgent: OzonetelAgentService, - ) { - this.graphqlUrl = config.get('platform.graphqlUrl')!; - this.workspaceSubdomain = process.env.PLATFORM_WORKSPACE_SUBDOMAIN ?? 'fortytwo-dev'; - this.origin = process.env.PLATFORM_ORIGIN ?? 'http://fortytwo-dev.localhost:4010'; - } + constructor( + private config: ConfigService, + private ozonetelAgent: OzonetelAgentService, + ) { + this.graphqlUrl = config.get('platform.graphqlUrl')!; + this.workspaceSubdomain = + process.env.PLATFORM_WORKSPACE_SUBDOMAIN ?? 'fortytwo-dev'; + this.origin = + process.env.PLATFORM_ORIGIN ?? 'http://fortytwo-dev.localhost:4010'; + } - @Post('login') - async login(@Body() body: { email: string; password: string }) { - this.logger.log(`Login attempt for ${body.email}`); + @Post('login') + async login(@Body() body: { email: string; password: string }) { + this.logger.log(`Login attempt for ${body.email}`); - try { - // Step 1: Get login token - const loginRes = await axios.post(this.graphqlUrl, { - query: `mutation GetLoginToken($email: String!, $password: String!) { + try { + // Step 1: Get login token + const loginRes = await axios.post( + this.graphqlUrl, + { + query: `mutation GetLoginToken($email: String!, $password: String!) { getLoginTokenFromCredentials( email: $email password: $password @@ -35,26 +39,31 @@ export class AuthController { loginToken { token } } }`, - variables: { email: body.email, password: body.password }, - }, { - headers: { - 'Content-Type': 'application/json', - 'X-Workspace-Subdomain': this.workspaceSubdomain, - }, - }); + variables: { email: body.email, password: body.password }, + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-Workspace-Subdomain': this.workspaceSubdomain, + }, + }, + ); - if (loginRes.data.errors) { - throw new HttpException( - loginRes.data.errors[0]?.message ?? 'Login failed', - 401, - ); - } + if (loginRes.data.errors) { + throw new HttpException( + loginRes.data.errors[0]?.message ?? 'Login failed', + 401, + ); + } - const loginToken = loginRes.data.data.getLoginTokenFromCredentials.loginToken.token; + const loginToken = + loginRes.data.data.getLoginTokenFromCredentials.loginToken.token; - // Step 2: Exchange for access + refresh tokens - const tokenRes = await axios.post(this.graphqlUrl, { - query: `mutation GetAuthTokens($loginToken: String!) { + // Step 2: Exchange for access + refresh tokens + const tokenRes = await axios.post( + this.graphqlUrl, + { + query: `mutation GetAuthTokens($loginToken: String!) { getAuthTokensFromLoginToken( loginToken: $loginToken origin: "${this.origin}" @@ -65,99 +74,114 @@ export class AuthController { } } }`, - variables: { loginToken }, - }, { - headers: { - 'Content-Type': 'application/json', - 'X-Workspace-Subdomain': this.workspaceSubdomain, - }, - }); + variables: { loginToken }, + }, + { + headers: { + 'Content-Type': 'application/json', + 'X-Workspace-Subdomain': this.workspaceSubdomain, + }, + }, + ); - if (tokenRes.data.errors) { - throw new HttpException( - tokenRes.data.errors[0]?.message ?? 'Token exchange failed', - 401, - ); - } + if (tokenRes.data.errors) { + throw new HttpException( + tokenRes.data.errors[0]?.message ?? 'Token exchange failed', + 401, + ); + } - const tokens = tokenRes.data.data.getAuthTokensFromLoginToken.tokens; - const accessToken = tokens.accessOrWorkspaceAgnosticToken.token; + const tokens = tokenRes.data.data.getAuthTokensFromLoginToken.tokens; + const accessToken = tokens.accessOrWorkspaceAgnosticToken.token; - // Step 3: Fetch user profile with roles - const profileRes = await axios.post(this.graphqlUrl, { - query: `{ currentUser { id email workspaceMember { id name { firstName lastName } userEmail avatarUrl roles { id label } } } }`, - }, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }); + // Step 3: Fetch user profile with roles + const profileRes = await axios.post( + this.graphqlUrl, + { + query: `{ currentUser { id email workspaceMember { id name { firstName lastName } userEmail avatarUrl roles { id label } } } }`, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ); - const currentUser = profileRes.data?.data?.currentUser; - const workspaceMember = currentUser?.workspaceMember; - const roles = workspaceMember?.roles ?? []; - const roleLabels = roles.map((r: any) => r.label); + const currentUser = profileRes.data?.data?.currentUser; + const workspaceMember = currentUser?.workspaceMember; + const roles = workspaceMember?.roles ?? []; + const roleLabels = roles.map((r: any) => r.label); - // Determine app role from platform roles - let appRole = 'executive'; // default - if (roleLabels.includes('HelixEngage Manager')) { - appRole = 'admin'; - } else if (roleLabels.includes('HelixEngage User')) { - // Distinguish CC agent from executive by email convention or config - // For now, emails containing 'cc' map to cc-agent - const email = workspaceMember?.userEmail ?? body.email; - appRole = email.includes('cc') ? 'cc-agent' : 'executive'; - } + // Determine app role from platform roles + let appRole = 'executive'; // default + if (roleLabels.includes('HelixEngage Manager')) { + appRole = 'admin'; + } else if (roleLabels.includes('HelixEngage User')) { + // Distinguish CC agent from executive by email convention or config + // For now, emails containing 'cc' map to cc-agent + const email = workspaceMember?.userEmail ?? body.email; + appRole = email.includes('cc') ? 'cc-agent' : 'executive'; + } - this.logger.log(`User ${body.email} logged in with role: ${appRole} (platform roles: ${roleLabels.join(', ')})`); + this.logger.log( + `User ${body.email} logged in with role: ${appRole} (platform roles: ${roleLabels.join(', ')})`, + ); - // Auto-login Ozonetel agent for CC agents (fire and forget) - if (appRole === 'cc-agent') { - const ozAgentId = process.env.OZONETEL_AGENT_ID ?? 'agent3'; - const ozAgentPassword = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$'; - const ozSipId = process.env.OZONETEL_SIP_ID ?? '521814'; + // Auto-login Ozonetel agent for CC agents (fire and forget) + if (appRole === 'cc-agent') { + const ozAgentId = process.env.OZONETEL_AGENT_ID ?? 'agent3'; + const ozAgentPassword = + process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$'; + const ozSipId = process.env.OZONETEL_SIP_ID ?? '521814'; - this.ozonetelAgent.loginAgent({ - agentId: ozAgentId, - password: ozAgentPassword, - phoneNumber: ozSipId, - mode: 'blended', - }).catch(err => { - this.logger.warn(`Ozonetel agent login failed (non-blocking): ${err.message}`); - }); - } + this.ozonetelAgent + .loginAgent({ + agentId: ozAgentId, + password: ozAgentPassword, + phoneNumber: ozSipId, + mode: 'blended', + }) + .catch((err) => { + this.logger.warn( + `Ozonetel agent login failed (non-blocking): ${err.message}`, + ); + }); + } - return { - accessToken, - refreshToken: tokens.refreshToken.token, - user: { - id: currentUser?.id, - email: currentUser?.email, - firstName: workspaceMember?.name?.firstName ?? '', - lastName: workspaceMember?.name?.lastName ?? '', - avatarUrl: workspaceMember?.avatarUrl, - role: appRole, - platformRoles: roleLabels, - }, - }; - } catch (error) { - if (error instanceof HttpException) throw error; - this.logger.error(`Login proxy failed: ${error}`); - throw new HttpException('Authentication service unavailable', 503); - } + return { + accessToken, + refreshToken: tokens.refreshToken.token, + user: { + id: currentUser?.id, + email: currentUser?.email, + firstName: workspaceMember?.name?.firstName ?? '', + lastName: workspaceMember?.name?.lastName ?? '', + avatarUrl: workspaceMember?.avatarUrl, + role: appRole, + platformRoles: roleLabels, + }, + }; + } catch (error) { + if (error instanceof HttpException) throw error; + this.logger.error(`Login proxy failed: ${error}`); + throw new HttpException('Authentication service unavailable', 503); + } + } + + @Post('refresh') + async refresh(@Body() body: { refreshToken: string }) { + if (!body.refreshToken) { + throw new HttpException('refreshToken required', 400); } - @Post('refresh') - async refresh(@Body() body: { refreshToken: string }) { - if (!body.refreshToken) { - throw new HttpException('refreshToken required', 400); - } + this.logger.log('Token refresh request'); - this.logger.log('Token refresh request'); - - try { - const res = await axios.post(this.graphqlUrl, { - query: `mutation RefreshToken($token: String!) { + try { + const res = await axios.post( + this.graphqlUrl, + { + query: `mutation RefreshToken($token: String!) { renewToken(appToken: $token) { tokens { accessOrWorkspaceAgnosticToken { token expiresAt } @@ -165,25 +189,29 @@ export class AuthController { } } }`, - variables: { token: body.refreshToken }, - }, { - headers: { 'Content-Type': 'application/json' }, - }); + variables: { token: body.refreshToken }, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); - if (res.data.errors) { - this.logger.warn(`Token refresh failed: ${res.data.errors[0]?.message}`); - throw new HttpException('Token refresh failed', 401); - } + if (res.data.errors) { + this.logger.warn( + `Token refresh failed: ${res.data.errors[0]?.message}`, + ); + throw new HttpException('Token refresh failed', 401); + } - const tokens = res.data.data.renewToken.tokens; - return { - accessToken: tokens.accessOrWorkspaceAgnosticToken.token, - refreshToken: tokens.refreshToken.token, - }; - } catch (error) { - if (error instanceof HttpException) throw error; - this.logger.error(`Token refresh failed: ${error}`); - throw new HttpException('Token refresh failed', 401); - } + const tokens = res.data.data.renewToken.tokens; + return { + accessToken: tokens.accessOrWorkspaceAgnosticToken.token, + refreshToken: tokens.refreshToken.token, + }; + } catch (error) { + if (error instanceof HttpException) throw error; + this.logger.error(`Token refresh failed: ${error}`); + throw new HttpException('Token refresh failed', 401); } + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 76842f5..b26eeab 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -3,7 +3,7 @@ import { AuthController } from './auth.controller'; import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module'; @Module({ - imports: [OzonetelAgentModule], - controllers: [AuthController], + imports: [OzonetelAgentModule], + controllers: [AuthController], }) export class AuthModule {} diff --git a/src/call-assist/call-assist.gateway.ts b/src/call-assist/call-assist.gateway.ts index 44b5d00..0aa9c98 100644 --- a/src/call-assist/call-assist.gateway.ts +++ b/src/call-assist/call-assist.gateway.ts @@ -1,9 +1,9 @@ import { - WebSocketGateway, - SubscribeMessage, - MessageBody, - ConnectedSocket, - OnGatewayDisconnect, + WebSocketGateway, + SubscribeMessage, + MessageBody, + ConnectedSocket, + OnGatewayDisconnect, } from '@nestjs/websockets'; import { Logger } from '@nestjs/common'; import { Socket } from 'socket.io'; @@ -11,126 +11,138 @@ import WebSocket from 'ws'; import { CallAssistService } from './call-assist.service'; type SessionState = { - deepgramWs: WebSocket | null; - transcript: string; - context: string; - suggestionTimer: NodeJS.Timeout | null; + deepgramWs: WebSocket | null; + transcript: string; + context: string; + suggestionTimer: NodeJS.Timeout | null; }; @WebSocketGateway({ - cors: { origin: process.env.CORS_ORIGIN ?? '*', credentials: true }, - namespace: '/call-assist', + cors: { origin: process.env.CORS_ORIGIN ?? '*', credentials: true }, + namespace: '/call-assist', }) export class CallAssistGateway implements OnGatewayDisconnect { - private readonly logger = new Logger(CallAssistGateway.name); - private readonly sessions = new Map(); - private readonly deepgramApiKey: string; + private readonly logger = new Logger(CallAssistGateway.name); + private readonly sessions = new Map(); + private readonly deepgramApiKey: string; - constructor(private readonly callAssist: CallAssistService) { - this.deepgramApiKey = process.env.DEEPGRAM_API_KEY ?? ''; + constructor(private readonly callAssist: CallAssistService) { + this.deepgramApiKey = process.env.DEEPGRAM_API_KEY ?? ''; + } + + @SubscribeMessage('call-assist:start') + async handleStart( + @ConnectedSocket() client: Socket, + @MessageBody() + data: { ucid: string; leadId?: string; callerPhone?: string }, + ) { + this.logger.log( + `Call assist start: ucid=${data.ucid} lead=${data.leadId ?? 'none'}`, + ); + + const context = await this.callAssist.loadCallContext( + data.leadId ?? null, + data.callerPhone ?? null, + ); + client.emit('call-assist:context', { + context: context.substring(0, 200) + '...', + }); + + const session: SessionState = { + deepgramWs: null, + transcript: '', + context, + suggestionTimer: null, + }; + + if (this.deepgramApiKey) { + const dgUrl = `wss://api.deepgram.com/v1/listen?model=nova-2&language=en&smart_format=true&interim_results=true&endpointing=300&sample_rate=16000&encoding=linear16&channels=1`; + + const dgWs = new WebSocket(dgUrl, { + headers: { Authorization: `Token ${this.deepgramApiKey}` }, + }); + + dgWs.on('open', () => { + this.logger.log(`Deepgram connected for ${data.ucid}`); + }); + + dgWs.on('message', (raw: WebSocket.Data) => { + try { + const result = JSON.parse(raw.toString()); + const text = result.channel?.alternatives?.[0]?.transcript; + if (!text) return; + + const isFinal = result.is_final; + client.emit('call-assist:transcript', { text, isFinal }); + + if (isFinal) { + session.transcript += `Customer: ${text}\n`; + } + } catch {} + }); + + dgWs.on('error', (err) => { + this.logger.error(`Deepgram error: ${err.message}`); + }); + + dgWs.on('close', () => { + this.logger.log(`Deepgram closed for ${data.ucid}`); + }); + + session.deepgramWs = dgWs; + } else { + this.logger.warn('DEEPGRAM_API_KEY not set — transcription disabled'); + client.emit('call-assist:error', { + message: 'Transcription not configured', + }); } - @SubscribeMessage('call-assist:start') - async handleStart( - @ConnectedSocket() client: Socket, - @MessageBody() data: { ucid: string; leadId?: string; callerPhone?: string }, - ) { - this.logger.log(`Call assist start: ucid=${data.ucid} lead=${data.leadId ?? 'none'}`); + // AI suggestion every 10 seconds + session.suggestionTimer = setInterval(async () => { + if (!session.transcript.trim()) return; + const suggestion = await this.callAssist.getSuggestion( + session.transcript, + session.context, + ); + if (suggestion) { + client.emit('call-assist:suggestion', { text: suggestion }); + } + }, 10000); - const context = await this.callAssist.loadCallContext( - data.leadId ?? null, - data.callerPhone ?? null, - ); - client.emit('call-assist:context', { context: context.substring(0, 200) + '...' }); + this.sessions.set(client.id, session); + } - const session: SessionState = { - deepgramWs: null, - transcript: '', - context, - suggestionTimer: null, - }; - - if (this.deepgramApiKey) { - const dgUrl = `wss://api.deepgram.com/v1/listen?model=nova-2&language=en&smart_format=true&interim_results=true&endpointing=300&sample_rate=16000&encoding=linear16&channels=1`; - - const dgWs = new WebSocket(dgUrl, { - headers: { Authorization: `Token ${this.deepgramApiKey}` }, - }); - - dgWs.on('open', () => { - this.logger.log(`Deepgram connected for ${data.ucid}`); - }); - - dgWs.on('message', (raw: WebSocket.Data) => { - try { - const result = JSON.parse(raw.toString()); - const text = result.channel?.alternatives?.[0]?.transcript; - if (!text) return; - - const isFinal = result.is_final; - client.emit('call-assist:transcript', { text, isFinal }); - - if (isFinal) { - session.transcript += `Customer: ${text}\n`; - } - } catch {} - }); - - dgWs.on('error', (err) => { - this.logger.error(`Deepgram error: ${err.message}`); - }); - - dgWs.on('close', () => { - this.logger.log(`Deepgram closed for ${data.ucid}`); - }); - - session.deepgramWs = dgWs; - } else { - this.logger.warn('DEEPGRAM_API_KEY not set — transcription disabled'); - client.emit('call-assist:error', { message: 'Transcription not configured' }); - } - - // AI suggestion every 10 seconds - session.suggestionTimer = setInterval(async () => { - if (!session.transcript.trim()) return; - const suggestion = await this.callAssist.getSuggestion(session.transcript, session.context); - if (suggestion) { - client.emit('call-assist:suggestion', { text: suggestion }); - } - }, 10000); - - this.sessions.set(client.id, session); + @SubscribeMessage('call-assist:audio') + handleAudio( + @ConnectedSocket() client: Socket, + @MessageBody() audioData: ArrayBuffer, + ) { + const session = this.sessions.get(client.id); + if (session?.deepgramWs?.readyState === WebSocket.OPEN) { + session.deepgramWs.send(Buffer.from(audioData)); } + } - @SubscribeMessage('call-assist:audio') - handleAudio( - @ConnectedSocket() client: Socket, - @MessageBody() audioData: ArrayBuffer, - ) { - const session = this.sessions.get(client.id); - if (session?.deepgramWs?.readyState === WebSocket.OPEN) { - session.deepgramWs.send(Buffer.from(audioData)); - } - } + @SubscribeMessage('call-assist:stop') + handleStop(@ConnectedSocket() client: Socket) { + this.cleanup(client.id); + this.logger.log(`Call assist stopped: ${client.id}`); + } - @SubscribeMessage('call-assist:stop') - handleStop(@ConnectedSocket() client: Socket) { - this.cleanup(client.id); - this.logger.log(`Call assist stopped: ${client.id}`); - } + handleDisconnect(client: Socket) { + this.cleanup(client.id); + } - handleDisconnect(client: Socket) { - this.cleanup(client.id); - } - - private cleanup(clientId: string) { - const session = this.sessions.get(clientId); - if (session) { - if (session.suggestionTimer) clearInterval(session.suggestionTimer); - if (session.deepgramWs) { - try { session.deepgramWs.close(); } catch {} - } - this.sessions.delete(clientId); - } + private cleanup(clientId: string) { + const session = this.sessions.get(clientId); + if (session) { + if (session.suggestionTimer) clearInterval(session.suggestionTimer); + if (session.deepgramWs) { + try { + session.deepgramWs.close(); + } catch {} + } + this.sessions.delete(clientId); } + } } diff --git a/src/call-assist/call-assist.module.ts b/src/call-assist/call-assist.module.ts index cec11c2..bc2b61d 100644 --- a/src/call-assist/call-assist.module.ts +++ b/src/call-assist/call-assist.module.ts @@ -4,7 +4,7 @@ import { CallAssistService } from './call-assist.service'; import { PlatformModule } from '../platform/platform.module'; @Module({ - imports: [PlatformModule], - providers: [CallAssistGateway, CallAssistService], + imports: [PlatformModule], + providers: [CallAssistGateway, CallAssistService], }) export class CallAssistModule {} diff --git a/src/call-assist/call-assist.service.ts b/src/call-assist/call-assist.service.ts index e20f577..34d8127 100644 --- a/src/call-assist/call-assist.service.ts +++ b/src/call-assist/call-assist.service.ts @@ -7,99 +7,119 @@ import type { LanguageModel } from 'ai'; @Injectable() export class CallAssistService { - private readonly logger = new Logger(CallAssistService.name); - private readonly aiModel: LanguageModel | null; - private readonly platformApiKey: string; + private readonly logger = new Logger(CallAssistService.name); + private readonly aiModel: LanguageModel | null; + private readonly platformApiKey: string; - constructor( - private config: ConfigService, - private platform: PlatformGraphqlService, - ) { - this.aiModel = createAiModel(config); - this.platformApiKey = config.get('platform.apiKey') ?? ''; - } + constructor( + private config: ConfigService, + private platform: PlatformGraphqlService, + ) { + this.aiModel = createAiModel(config); + this.platformApiKey = config.get('platform.apiKey') ?? ''; + } - async loadCallContext(leadId: string | null, callerPhone: string | null): Promise { - const authHeader = this.platformApiKey ? `Bearer ${this.platformApiKey}` : ''; - if (!authHeader) return 'No platform context available.'; + async loadCallContext( + leadId: string | null, + callerPhone: string | null, + ): Promise { + const authHeader = this.platformApiKey + ? `Bearer ${this.platformApiKey}` + : ''; + if (!authHeader) return 'No platform context available.'; - try { - const parts: string[] = []; + try { + const parts: string[] = []; - if (leadId) { - const leadResult = await this.platform.queryWithAuth( - `{ leads(filter: { id: { eq: "${leadId}" } }) { edges { node { + if (leadId) { + const leadResult = await this.platform.queryWithAuth( + `{ leads(filter: { id: { eq: "${leadId}" } }) { edges { node { id name contactName { firstName lastName } contactPhone { primaryPhoneNumber } source status interestedService lastContacted contactAttempts aiSummary aiSuggestedAction } } } }`, - undefined, authHeader, - ); - const lead = leadResult.leads.edges[0]?.node; - if (lead) { - const name = lead.contactName - ? `${lead.contactName.firstName} ${lead.contactName.lastName}`.trim() - : lead.name; - parts.push(`CALLER: ${name}`); - parts.push(`Phone: ${lead.contactPhone?.primaryPhoneNumber ?? callerPhone}`); - parts.push(`Source: ${lead.source ?? 'Unknown'}`); - parts.push(`Interested in: ${lead.interestedService ?? 'Not specified'}`); - parts.push(`Contact attempts: ${lead.contactAttempts ?? 0}`); - if (lead.aiSummary) parts.push(`AI Summary: ${lead.aiSummary}`); - } + undefined, + authHeader, + ); + const lead = leadResult.leads.edges[0]?.node; + if (lead) { + const name = lead.contactName + ? `${lead.contactName.firstName} ${lead.contactName.lastName}`.trim() + : lead.name; + parts.push(`CALLER: ${name}`); + parts.push( + `Phone: ${lead.contactPhone?.primaryPhoneNumber ?? callerPhone}`, + ); + parts.push(`Source: ${lead.source ?? 'Unknown'}`); + parts.push( + `Interested in: ${lead.interestedService ?? 'Not specified'}`, + ); + parts.push(`Contact attempts: ${lead.contactAttempts ?? 0}`); + if (lead.aiSummary) parts.push(`AI Summary: ${lead.aiSummary}`); + } - const apptResult = await this.platform.queryWithAuth( - `{ appointments(first: 10, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { + const apptResult = await this.platform.queryWithAuth( + `{ appointments(first: 10, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { id scheduledAt appointmentStatus doctorName department reasonForVisit patientId } } } }`, - undefined, authHeader, - ); - const appts = apptResult.appointments.edges - .map((e: any) => e.node) - .filter((a: any) => a.patientId === leadId); - if (appts.length > 0) { - parts.push('\nPAST APPOINTMENTS:'); - for (const a of appts) { - const date = a.scheduledAt ? new Date(a.scheduledAt).toLocaleDateString('en-IN') : '?'; - parts.push(`- ${date}: ${a.doctorName ?? '?'} (${a.department ?? '?'}) — ${a.appointmentStatus}`); - } - } - } else if (callerPhone) { - parts.push(`CALLER: Unknown (${callerPhone})`); - parts.push('No lead record found — this may be a new enquiry.'); - } + undefined, + authHeader, + ); + const appts = apptResult.appointments.edges + .map((e: any) => e.node) + .filter((a: any) => a.patientId === leadId); + if (appts.length > 0) { + parts.push('\nPAST APPOINTMENTS:'); + for (const a of appts) { + const date = a.scheduledAt + ? new Date(a.scheduledAt).toLocaleDateString('en-IN') + : '?'; + parts.push( + `- ${date}: ${a.doctorName ?? '?'} (${a.department ?? '?'}) — ${a.appointmentStatus}`, + ); + } + } + } else if (callerPhone) { + parts.push(`CALLER: Unknown (${callerPhone})`); + parts.push('No lead record found — this may be a new enquiry.'); + } - const docResult = await this.platform.queryWithAuth( - `{ doctors(first: 20) { edges { node { + const docResult = await this.platform.queryWithAuth( + `{ doctors(first: 20) { edges { node { fullName { firstName lastName } department specialty clinic { clinicName } } } } }`, - undefined, authHeader, - ); - const docs = docResult.doctors.edges.map((e: any) => e.node); - if (docs.length > 0) { - parts.push('\nAVAILABLE DOCTORS:'); - for (const d of docs) { - const name = d.fullName ? `Dr. ${d.fullName.firstName} ${d.fullName.lastName}`.trim() : 'Unknown'; - parts.push(`- ${name} — ${d.department ?? '?'} — ${d.clinic?.clinicName ?? '?'}`); - } - } - - return parts.join('\n') || 'No context available.'; - } catch (err) { - this.logger.error(`Failed to load call context: ${err}`); - return 'Context loading failed.'; + undefined, + authHeader, + ); + const docs = docResult.doctors.edges.map((e: any) => e.node); + if (docs.length > 0) { + parts.push('\nAVAILABLE DOCTORS:'); + for (const d of docs) { + const name = d.fullName + ? `Dr. ${d.fullName.firstName} ${d.fullName.lastName}`.trim() + : 'Unknown'; + parts.push( + `- ${name} — ${d.department ?? '?'} — ${d.clinic?.clinicName ?? '?'}`, + ); } + } + + return parts.join('\n') || 'No context available.'; + } catch (err) { + this.logger.error(`Failed to load call context: ${err}`); + return 'Context loading failed.'; } + } - async getSuggestion(transcript: string, context: string): Promise { - if (!this.aiModel || !transcript.trim()) return ''; + async getSuggestion(transcript: string, context: string): Promise { + if (!this.aiModel || !transcript.trim()) return ''; - try { - const { text } = await generateText({ - model: this.aiModel, - system: `You are a real-time call assistant for Global Hospital Bangalore. + try { + const { text } = await generateText({ + model: this.aiModel, + system: `You are a real-time call assistant for Global Hospital Bangalore. You listen to the customer's words and provide brief, actionable suggestions for the CC agent. ${context} @@ -111,13 +131,13 @@ RULES: - If customer wants to cancel or reschedule, note relevant appointment details - If customer sounds upset, suggest empathetic response - Do NOT repeat what the agent already knows`, - prompt: `Conversation transcript so far:\n${transcript}\n\nProvide a brief suggestion for the agent based on what was just said.`, - maxOutputTokens: 150, - }); - return text; - } catch (err) { - this.logger.error(`AI suggestion failed: ${err}`); - return ''; - } + prompt: `Conversation transcript so far:\n${transcript}\n\nProvide a brief suggestion for the agent based on what was just said.`, + maxOutputTokens: 150, + }); + return text; + } catch (err) { + this.logger.error(`AI suggestion failed: ${err}`); + return ''; } + } } diff --git a/src/call-events/call-events.gateway.ts b/src/call-events/call-events.gateway.ts index 1efdcf2..3d658d0 100644 --- a/src/call-events/call-events.gateway.ts +++ b/src/call-events/call-events.gateway.ts @@ -1,76 +1,79 @@ import { - WebSocketGateway, - WebSocketServer, - SubscribeMessage, - MessageBody, - ConnectedSocket, + WebSocketGateway, + WebSocketServer, + SubscribeMessage, + MessageBody, + ConnectedSocket, } from '@nestjs/websockets'; import { Logger, Inject, forwardRef } from '@nestjs/common'; import { Server, Socket } from 'socket.io'; -import type { EnrichedCallEvent, DispositionPayload } from './call-events.types'; +import type { + EnrichedCallEvent, + DispositionPayload, +} from './call-events.types'; import { CallEventsService } from './call-events.service'; @WebSocketGateway({ - cors: { - origin: process.env.CORS_ORIGIN ?? 'http://localhost:5173', - credentials: true, - }, - namespace: '/call-events', + cors: { + origin: process.env.CORS_ORIGIN ?? 'http://localhost:5173', + credentials: true, + }, + namespace: '/call-events', }) export class CallEventsGateway { - @WebSocketServer() - server: Server; + @WebSocketServer() + server: Server; - private readonly logger = new Logger(CallEventsGateway.name); + private readonly logger = new Logger(CallEventsGateway.name); - constructor( - @Inject(forwardRef(() => CallEventsService)) - private readonly callEventsService: CallEventsService, - ) {} + constructor( + @Inject(forwardRef(() => CallEventsService)) + private readonly callEventsService: CallEventsService, + ) {} - // Push enriched call event to a specific agent's room - pushCallEvent(agentName: string, event: EnrichedCallEvent) { - const room = `agent:${agentName}`; - this.logger.log(`Pushing ${event.eventType} event to room ${room}`); - this.server.to(room).emit('call:incoming', event); - } + // Push enriched call event to a specific agent's room + pushCallEvent(agentName: string, event: EnrichedCallEvent) { + const room = `agent:${agentName}`; + this.logger.log(`Pushing ${event.eventType} event to room ${room}`); + this.server.to(room).emit('call:incoming', event); + } - // Agent registers when they open the Call Desk page - @SubscribeMessage('agent:register') - handleAgentRegister( - @ConnectedSocket() client: Socket, - @MessageBody() agentName: string, - ) { - const room = `agent:${agentName}`; - client.join(room); - this.logger.log( - `Agent ${agentName} registered in room ${room} (socket: ${client.id})`, - ); - client.emit('agent:registered', { agentName, room }); - } + // Agent registers when they open the Call Desk page + @SubscribeMessage('agent:register') + handleAgentRegister( + @ConnectedSocket() client: Socket, + @MessageBody() agentName: string, + ) { + const room = `agent:${agentName}`; + client.join(room); + this.logger.log( + `Agent ${agentName} registered in room ${room} (socket: ${client.id})`, + ); + client.emit('agent:registered', { agentName, room }); + } - // Agent sends disposition after a call - @SubscribeMessage('call:disposition') - async handleDisposition( - @ConnectedSocket() client: Socket, - @MessageBody() payload: DispositionPayload, - ) { - this.logger.log( - `Disposition received from ${payload.agentName}: ${payload.disposition}`, - ); - await this.callEventsService.handleDisposition(payload); - client.emit('call:disposition:ack', { - status: 'saved', - callSid: payload.callSid, - }); - return payload; - } + // Agent sends disposition after a call + @SubscribeMessage('call:disposition') + async handleDisposition( + @ConnectedSocket() client: Socket, + @MessageBody() payload: DispositionPayload, + ) { + this.logger.log( + `Disposition received from ${payload.agentName}: ${payload.disposition}`, + ); + await this.callEventsService.handleDisposition(payload); + client.emit('call:disposition:ack', { + status: 'saved', + callSid: payload.callSid, + }); + return payload; + } - handleConnection(client: Socket) { - this.logger.log(`Client connected: ${client.id}`); - } + handleConnection(client: Socket) { + this.logger.log(`Client connected: ${client.id}`); + } - handleDisconnect(client: Socket) { - this.logger.log(`Client disconnected: ${client.id}`); - } + handleDisconnect(client: Socket) { + this.logger.log(`Client disconnected: ${client.id}`); + } } diff --git a/src/call-events/call-events.module.ts b/src/call-events/call-events.module.ts index 5eea515..356ed3b 100644 --- a/src/call-events/call-events.module.ts +++ b/src/call-events/call-events.module.ts @@ -6,9 +6,9 @@ import { CallEventsGateway } from './call-events.gateway'; import { CallLookupController } from './call-lookup.controller'; @Module({ - imports: [PlatformModule, AiModule], - controllers: [CallLookupController], - providers: [CallEventsService, CallEventsGateway], - exports: [CallEventsService, CallEventsGateway], + imports: [PlatformModule, AiModule], + controllers: [CallLookupController], + providers: [CallEventsService, CallEventsGateway], + exports: [CallEventsService, CallEventsGateway], }) export class CallEventsModule {} diff --git a/src/call-events/call-events.service.ts b/src/call-events/call-events.service.ts index 2f2c787..ec6cc77 100644 --- a/src/call-events/call-events.service.ts +++ b/src/call-events/call-events.service.ts @@ -4,231 +4,217 @@ import { AiEnrichmentService } from '../ai/ai-enrichment.service'; import { CallEventsGateway } from './call-events.gateway'; import type { CallEvent } from '../exotel/exotel.types'; import type { - EnrichedCallEvent, - DispositionPayload, + EnrichedCallEvent, + DispositionPayload, } from './call-events.types'; const DISPOSITION_TO_LEAD_STATUS: Record = { - APPOINTMENT_BOOKED: 'APPOINTMENT_SET', - FOLLOW_UP_SCHEDULED: 'CONTACTED', - INFO_PROVIDED: 'CONTACTED', - CALLBACK_REQUESTED: 'CONTACTED', - WRONG_NUMBER: 'LOST', - NO_ANSWER: 'CONTACTED', - NOT_INTERESTED: 'LOST', + APPOINTMENT_BOOKED: 'APPOINTMENT_SET', + FOLLOW_UP_SCHEDULED: 'CONTACTED', + INFO_PROVIDED: 'CONTACTED', + CALLBACK_REQUESTED: 'CONTACTED', + WRONG_NUMBER: 'LOST', + NO_ANSWER: 'CONTACTED', + NOT_INTERESTED: 'LOST', }; @Injectable() export class CallEventsService { - private readonly logger = new Logger(CallEventsService.name); + private readonly logger = new Logger(CallEventsService.name); - constructor( - private readonly platform: PlatformGraphqlService, - private readonly ai: AiEnrichmentService, - @Inject(forwardRef(() => CallEventsGateway)) - private readonly gateway: CallEventsGateway, - ) {} + constructor( + private readonly platform: PlatformGraphqlService, + private readonly ai: AiEnrichmentService, + @Inject(forwardRef(() => CallEventsGateway)) + private readonly gateway: CallEventsGateway, + ) {} - async handleIncomingCall(callEvent: CallEvent): Promise { + async handleIncomingCall(callEvent: CallEvent): Promise { + this.logger.log( + `Processing incoming call from ${callEvent.callerPhone} to agent ${callEvent.agentName}`, + ); + + // 1. Lookup lead by phone + let lead = null; + try { + lead = await this.platform.findLeadByPhone(callEvent.callerPhone); + if (lead) { this.logger.log( - `Processing incoming call from ${callEvent.callerPhone} to agent ${callEvent.agentName}`, + `Matched lead: ${lead.contactName?.firstName} ${lead.contactName?.lastName} (${lead.id})`, ); + } else { + this.logger.log(`No lead found for phone ${callEvent.callerPhone}`); + } + } catch (error) { + this.logger.error(`Lead lookup failed: ${error}`); + } - // 1. Lookup lead by phone - let lead = null; + // 2. AI enrichment (if lead found and no existing summary) + if (lead && !lead.aiSummary) { + try { + const activities = await this.platform.getLeadActivities(lead.id, 5); + const enrichment = await this.ai.enrichLead({ + firstName: lead.contactName?.firstName, + lastName: lead.contactName?.lastName, + leadSource: lead.leadSource ?? undefined, + interestedService: lead.interestedService ?? undefined, + leadStatus: lead.leadStatus ?? undefined, + contactAttempts: lead.contactAttempts ?? undefined, + createdAt: lead.createdAt, + activities: activities.map((a) => ({ + activityType: a.activityType ?? '', + summary: a.summary ?? '', + })), + }); + + // Persist AI enrichment back to platform + await this.platform.updateLead(lead.id, enrichment); + lead.aiSummary = enrichment.aiSummary; + lead.aiSuggestedAction = enrichment.aiSuggestedAction; + + this.logger.log(`AI enrichment applied for lead ${lead.id}`); + } catch (error) { + this.logger.error(`AI enrichment failed: ${error}`); + } + } + + // 3. Get recent activities for display + let recentActivities: { + activityType: string; + summary: string; + occurredAt: string; + performedBy: string; + }[] = []; + if (lead) { + try { + const activities = await this.platform.getLeadActivities(lead.id, 3); + recentActivities = activities.map((a) => ({ + activityType: a.activityType ?? '', + summary: a.summary ?? '', + occurredAt: a.occurredAt ?? '', + performedBy: a.performedBy ?? '', + })); + } catch (error) { + this.logger.error(`Failed to fetch activities: ${error}`); + } + } + + // 4. Build enriched event + const daysSinceCreation = lead?.createdAt + ? Math.floor( + (Date.now() - new Date(lead.createdAt).getTime()) / + (1000 * 60 * 60 * 24), + ) + : 0; + + const enrichedEvent: EnrichedCallEvent = { + callSid: callEvent.exotelCallSid, + eventType: callEvent.eventType, + lead: lead + ? { + id: lead.id, + firstName: lead.contactName?.firstName ?? 'Unknown', + lastName: lead.contactName?.lastName ?? '', + phone: lead.contactPhone?.[0] + ? `${lead.contactPhone[0].callingCode} ${lead.contactPhone[0].number}` + : callEvent.callerPhone, + email: lead.contactEmail?.[0]?.address, + source: lead.leadSource ?? undefined, + status: lead.leadStatus ?? undefined, + interestedService: lead.interestedService ?? undefined, + age: daysSinceCreation, + aiSummary: lead.aiSummary ?? undefined, + aiSuggestedAction: lead.aiSuggestedAction ?? undefined, + recentActivities, + } + : null, + callerPhone: callEvent.callerPhone, + agentName: callEvent.agentName, + timestamp: callEvent.timestamp, + }; + + // 5. Push to agent's browser via WebSocket + this.gateway.pushCallEvent(callEvent.agentName, enrichedEvent); + } + + async handleCallEnded(callEvent: CallEvent): Promise { + this.logger.log(`Call ended: ${callEvent.exotelCallSid}`); + + const enrichedEvent: EnrichedCallEvent = { + callSid: callEvent.exotelCallSid, + eventType: 'ended', + lead: null, + callerPhone: callEvent.callerPhone, + agentName: callEvent.agentName, + timestamp: callEvent.timestamp, + }; + + this.gateway.pushCallEvent(callEvent.agentName, enrichedEvent); + } + + async handleDisposition(payload: DispositionPayload): Promise { + this.logger.log( + `Processing disposition: ${payload.disposition} for call ${payload.callSid}`, + ); + + // 1. Create Call record in platform + try { + await this.platform.createCall({ + callDirection: 'INBOUND', + callStatus: 'COMPLETED', + callerNumber: payload.callerPhone + ? [ + { + number: payload.callerPhone.replace(/\D/g, ''), + callingCode: '+91', + }, + ] + : undefined, + agentName: payload.agentName, + startedAt: payload.startedAt, + endedAt: new Date().toISOString(), + durationSeconds: payload.duration, + disposition: payload.disposition, + callNotes: payload.notes || undefined, + leadId: payload.leadId || undefined, + }); + this.logger.log(`Call record created for ${payload.callSid}`); + } catch (error) { + this.logger.error(`Failed to create call record: ${error}`); + } + + // 2. Update lead status based on disposition + if (payload.leadId) { + const newStatus = DISPOSITION_TO_LEAD_STATUS[payload.disposition]; + if (newStatus) { try { - lead = await this.platform.findLeadByPhone(callEvent.callerPhone); - if (lead) { - this.logger.log( - `Matched lead: ${lead.contactName?.firstName} ${lead.contactName?.lastName} (${lead.id})`, - ); - } else { - this.logger.log( - `No lead found for phone ${callEvent.callerPhone}`, - ); - } + await this.platform.updateLead(payload.leadId, { + leadStatus: newStatus, + lastContactedAt: new Date().toISOString(), + }); + this.logger.log( + `Lead ${payload.leadId} status updated to ${newStatus}`, + ); } catch (error) { - this.logger.error(`Lead lookup failed: ${error}`); + this.logger.error(`Failed to update lead: ${error}`); } + } - // 2. AI enrichment (if lead found and no existing summary) - if (lead && !lead.aiSummary) { - try { - const activities = await this.platform.getLeadActivities( - lead.id, - 5, - ); - const enrichment = await this.ai.enrichLead({ - firstName: lead.contactName?.firstName, - lastName: lead.contactName?.lastName, - leadSource: lead.leadSource ?? undefined, - interestedService: lead.interestedService ?? undefined, - leadStatus: lead.leadStatus ?? undefined, - contactAttempts: lead.contactAttempts ?? undefined, - createdAt: lead.createdAt, - activities: activities.map((a) => ({ - activityType: a.activityType ?? '', - summary: a.summary ?? '', - })), - }); - - // Persist AI enrichment back to platform - await this.platform.updateLead(lead.id, enrichment); - lead.aiSummary = enrichment.aiSummary; - lead.aiSuggestedAction = enrichment.aiSuggestedAction; - - this.logger.log(`AI enrichment applied for lead ${lead.id}`); - } catch (error) { - this.logger.error(`AI enrichment failed: ${error}`); - } - } - - // 3. Get recent activities for display - let recentActivities: { - activityType: string; - summary: string; - occurredAt: string; - performedBy: string; - }[] = []; - if (lead) { - try { - const activities = await this.platform.getLeadActivities( - lead.id, - 3, - ); - recentActivities = activities.map((a) => ({ - activityType: a.activityType ?? '', - summary: a.summary ?? '', - occurredAt: a.occurredAt ?? '', - performedBy: a.performedBy ?? '', - })); - } catch (error) { - this.logger.error(`Failed to fetch activities: ${error}`); - } - } - - // 4. Build enriched event - const daysSinceCreation = lead?.createdAt - ? Math.floor( - (Date.now() - new Date(lead.createdAt).getTime()) / - (1000 * 60 * 60 * 24), - ) - : 0; - - const enrichedEvent: EnrichedCallEvent = { - callSid: callEvent.exotelCallSid, - eventType: callEvent.eventType, - lead: lead - ? { - id: lead.id, - firstName: lead.contactName?.firstName ?? 'Unknown', - lastName: lead.contactName?.lastName ?? '', - phone: lead.contactPhone?.[0] - ? `${lead.contactPhone[0].callingCode} ${lead.contactPhone[0].number}` - : callEvent.callerPhone, - email: lead.contactEmail?.[0]?.address, - source: lead.leadSource ?? undefined, - status: lead.leadStatus ?? undefined, - interestedService: - lead.interestedService ?? undefined, - age: daysSinceCreation, - aiSummary: lead.aiSummary ?? undefined, - aiSuggestedAction: - lead.aiSuggestedAction ?? undefined, - recentActivities, - } - : null, - callerPhone: callEvent.callerPhone, - agentName: callEvent.agentName, - timestamp: callEvent.timestamp, - }; - - // 5. Push to agent's browser via WebSocket - this.gateway.pushCallEvent(callEvent.agentName, enrichedEvent); - } - - async handleCallEnded(callEvent: CallEvent): Promise { - this.logger.log(`Call ended: ${callEvent.exotelCallSid}`); - - const enrichedEvent: EnrichedCallEvent = { - callSid: callEvent.exotelCallSid, - eventType: 'ended', - lead: null, - callerPhone: callEvent.callerPhone, - agentName: callEvent.agentName, - timestamp: callEvent.timestamp, - }; - - this.gateway.pushCallEvent(callEvent.agentName, enrichedEvent); - } - - async handleDisposition(payload: DispositionPayload): Promise { - this.logger.log( - `Processing disposition: ${payload.disposition} for call ${payload.callSid}`, - ); - - // 1. Create Call record in platform - try { - await this.platform.createCall({ - callDirection: 'INBOUND', - callStatus: 'COMPLETED', - callerNumber: payload.callerPhone - ? [ - { - number: payload.callerPhone.replace(/\D/g, ''), - callingCode: '+91', - }, - ] - : undefined, - agentName: payload.agentName, - startedAt: payload.startedAt, - endedAt: new Date().toISOString(), - durationSeconds: payload.duration, - disposition: payload.disposition, - callNotes: payload.notes || undefined, - leadId: payload.leadId || undefined, - }); - this.logger.log(`Call record created for ${payload.callSid}`); - } catch (error) { - this.logger.error(`Failed to create call record: ${error}`); - } - - // 2. Update lead status based on disposition - if (payload.leadId) { - const newStatus = DISPOSITION_TO_LEAD_STATUS[payload.disposition]; - if (newStatus) { - try { - await this.platform.updateLead(payload.leadId, { - leadStatus: newStatus, - lastContactedAt: new Date().toISOString(), - }); - this.logger.log( - `Lead ${payload.leadId} status updated to ${newStatus}`, - ); - } catch (error) { - this.logger.error(`Failed to update lead: ${error}`); - } - } - - // 3. Create lead activity - try { - await this.platform.createLeadActivity({ - activityType: 'CALL_RECEIVED', - summary: `Inbound call — ${payload.disposition.replace(/_/g, ' ')}`, - occurredAt: new Date().toISOString(), - performedBy: payload.agentName, - channel: 'PHONE', - durationSeconds: payload.duration, - leadId: payload.leadId, - }); - this.logger.log( - `Lead activity logged for ${payload.leadId}`, - ); - } catch (error) { - this.logger.error( - `Failed to create lead activity: ${error}`, - ); - } - } + // 3. Create lead activity + try { + await this.platform.createLeadActivity({ + activityType: 'CALL_RECEIVED', + summary: `Inbound call — ${payload.disposition.replace(/_/g, ' ')}`, + occurredAt: new Date().toISOString(), + performedBy: payload.agentName, + channel: 'PHONE', + durationSeconds: payload.duration, + leadId: payload.leadId, + }); + this.logger.log(`Lead activity logged for ${payload.leadId}`); + } catch (error) { + this.logger.error(`Failed to create lead activity: ${error}`); + } } + } } diff --git a/src/call-events/call-events.types.ts b/src/call-events/call-events.types.ts index 30f7d3e..dd50ce7 100644 --- a/src/call-events/call-events.types.ts +++ b/src/call-events/call-events.types.ts @@ -1,38 +1,38 @@ export type EnrichedCallEvent = { - callSid: string; - eventType: 'ringing' | 'answered' | 'ended'; - lead: { - id: string; - firstName: string; - lastName: string; - phone: string; - email?: string; - source?: string; - status?: string; - campaign?: string; - interestedService?: string; - age: number; - aiSummary?: string; - aiSuggestedAction?: string; - recentActivities: { - activityType: string; - summary: string; - occurredAt: string; - performedBy: string; - }[]; - } | null; - callerPhone: string; - agentName: string; - timestamp: string; + callSid: string; + eventType: 'ringing' | 'answered' | 'ended'; + lead: { + id: string; + firstName: string; + lastName: string; + phone: string; + email?: string; + source?: string; + status?: string; + campaign?: string; + interestedService?: string; + age: number; + aiSummary?: string; + aiSuggestedAction?: string; + recentActivities: { + activityType: string; + summary: string; + occurredAt: string; + performedBy: string; + }[]; + } | null; + callerPhone: string; + agentName: string; + timestamp: string; }; export type DispositionPayload = { - callSid: string; - leadId: string | null; - disposition: string; - notes: string; - agentName: string; - callerPhone: string; - startedAt: string; - duration: number; + callSid: string; + leadId: string | null; + disposition: string; + notes: string; + agentName: string; + callerPhone: string; + startedAt: string; + duration: number; }; diff --git a/src/call-events/call-lookup.controller.ts b/src/call-events/call-lookup.controller.ts index 331caa4..1c40d2c 100644 --- a/src/call-events/call-lookup.controller.ts +++ b/src/call-events/call-lookup.controller.ts @@ -1,88 +1,105 @@ -import { Controller, Post, Body, Logger, Headers, HttpException } from '@nestjs/common'; +import { + Controller, + Post, + Body, + Logger, + Headers, + HttpException, +} from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { AiEnrichmentService } from '../ai/ai-enrichment.service'; @Controller('api/call') export class CallLookupController { - private readonly logger = new Logger(CallLookupController.name); + private readonly logger = new Logger(CallLookupController.name); - constructor( - private readonly platform: PlatformGraphqlService, - private readonly ai: AiEnrichmentService, - ) {} + constructor( + private readonly platform: PlatformGraphqlService, + private readonly ai: AiEnrichmentService, + ) {} - @Post('lookup') - async lookupCaller( - @Body() body: { phoneNumber: string }, - @Headers('authorization') authHeader: string, - ) { - if (!authHeader) throw new HttpException('Authorization required', 401); - if (!body.phoneNumber) throw new HttpException('phoneNumber required', 400); + @Post('lookup') + async lookupCaller( + @Body() body: { phoneNumber: string }, + @Headers('authorization') authHeader: string, + ) { + if (!authHeader) throw new HttpException('Authorization required', 401); + if (!body.phoneNumber) throw new HttpException('phoneNumber required', 400); - const phone = body.phoneNumber.replace(/^0+/, ''); - this.logger.log(`Looking up caller: ${phone}`); + const phone = body.phoneNumber.replace(/^0+/, ''); + this.logger.log(`Looking up caller: ${phone}`); - // Query platform for leads matching this phone number - let lead = null; - let activities: any[] = []; + // Query platform for leads matching this phone number + let lead = null; + let activities: any[] = []; - try { - lead = await this.platform.findLeadByPhoneWithToken(phone, authHeader); - } catch (err) { - this.logger.warn(`Lead lookup failed: ${err}`); - } - - if (lead) { - this.logger.log(`Matched lead: ${lead.id} — ${lead.contactName?.firstName} ${lead.contactName?.lastName}`); - - // Get recent activities - try { - activities = await this.platform.getLeadActivitiesWithToken(lead.id, authHeader, 5); - } catch (err) { - this.logger.warn(`Activity fetch failed: ${err}`); - } - - // AI enrichment if no existing summary - if (!lead.aiSummary) { - try { - const enrichment = await this.ai.enrichLead({ - firstName: lead.contactName?.firstName, - lastName: lead.contactName?.lastName, - leadSource: lead.leadSource ?? undefined, - interestedService: lead.interestedService ?? undefined, - leadStatus: lead.leadStatus ?? undefined, - contactAttempts: lead.contactAttempts ?? undefined, - createdAt: lead.createdAt, - activities: activities.map((a: any) => ({ - activityType: a.activityType ?? '', - summary: a.summary ?? '', - })), - }); - - lead.aiSummary = enrichment.aiSummary; - lead.aiSuggestedAction = enrichment.aiSuggestedAction; - - // Persist AI enrichment back to platform - try { - await this.platform.updateLeadWithToken(lead.id, { - aiSummary: enrichment.aiSummary, - aiSuggestedAction: enrichment.aiSuggestedAction, - }, authHeader); - } catch (err) { - this.logger.warn(`Failed to persist AI enrichment: ${err}`); - } - } catch (err) { - this.logger.warn(`AI enrichment failed: ${err}`); - } - } - } else { - this.logger.log(`No lead found for phone ${phone}`); - } - - return { - lead, - activities, - matched: lead !== null, - }; + try { + lead = await this.platform.findLeadByPhoneWithToken(phone, authHeader); + } catch (err) { + this.logger.warn(`Lead lookup failed: ${err}`); } + + if (lead) { + this.logger.log( + `Matched lead: ${lead.id} — ${lead.contactName?.firstName} ${lead.contactName?.lastName}`, + ); + + // Get recent activities + try { + activities = await this.platform.getLeadActivitiesWithToken( + lead.id, + authHeader, + 5, + ); + } catch (err) { + this.logger.warn(`Activity fetch failed: ${err}`); + } + + // AI enrichment if no existing summary + if (!lead.aiSummary) { + try { + const enrichment = await this.ai.enrichLead({ + firstName: lead.contactName?.firstName, + lastName: lead.contactName?.lastName, + leadSource: lead.leadSource ?? undefined, + interestedService: lead.interestedService ?? undefined, + leadStatus: lead.leadStatus ?? undefined, + contactAttempts: lead.contactAttempts ?? undefined, + createdAt: lead.createdAt, + activities: activities.map((a: any) => ({ + activityType: a.activityType ?? '', + summary: a.summary ?? '', + })), + }); + + lead.aiSummary = enrichment.aiSummary; + lead.aiSuggestedAction = enrichment.aiSuggestedAction; + + // Persist AI enrichment back to platform + try { + await this.platform.updateLeadWithToken( + lead.id, + { + aiSummary: enrichment.aiSummary, + aiSuggestedAction: enrichment.aiSuggestedAction, + }, + authHeader, + ); + } catch (err) { + this.logger.warn(`Failed to persist AI enrichment: ${err}`); + } + } catch (err) { + this.logger.warn(`AI enrichment failed: ${err}`); + } + } + } else { + this.logger.log(`No lead found for phone ${phone}`); + } + + return { + lead, + activities, + matched: lead !== null, + }; + } } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 638f5fa..5ed1011 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -1,24 +1,28 @@ export default () => ({ - port: parseInt(process.env.PORT ?? '4100', 10), - corsOrigin: process.env.CORS_ORIGIN ?? 'http://localhost:5173', - platform: { - graphqlUrl: process.env.PLATFORM_GRAPHQL_URL ?? 'http://localhost:4000/graphql', - apiKey: process.env.PLATFORM_API_KEY ?? '', - }, - exotel: { - apiKey: process.env.EXOTEL_API_KEY ?? '', - apiToken: process.env.EXOTEL_API_TOKEN ?? '', - accountSid: process.env.EXOTEL_ACCOUNT_SID ?? '', - subdomain: process.env.EXOTEL_SUBDOMAIN ?? 'api.exotel.com', - webhookSecret: process.env.EXOTEL_WEBHOOK_SECRET ?? '', - }, - missedQueue: { - pollIntervalMs: parseInt(process.env.MISSED_QUEUE_POLL_INTERVAL_MS ?? '30000', 10), - }, - ai: { - provider: process.env.AI_PROVIDER ?? 'openai', - anthropicApiKey: process.env.ANTHROPIC_API_KEY ?? '', - openaiApiKey: process.env.OPENAI_API_KEY ?? '', - model: process.env.AI_MODEL ?? 'gpt-4o-mini', - }, + port: parseInt(process.env.PORT ?? '4100', 10), + corsOrigin: process.env.CORS_ORIGIN ?? 'http://localhost:5173', + platform: { + graphqlUrl: + process.env.PLATFORM_GRAPHQL_URL ?? 'http://localhost:4000/graphql', + apiKey: process.env.PLATFORM_API_KEY ?? '', + }, + exotel: { + apiKey: process.env.EXOTEL_API_KEY ?? '', + apiToken: process.env.EXOTEL_API_TOKEN ?? '', + accountSid: process.env.EXOTEL_ACCOUNT_SID ?? '', + subdomain: process.env.EXOTEL_SUBDOMAIN ?? 'api.exotel.com', + webhookSecret: process.env.EXOTEL_WEBHOOK_SECRET ?? '', + }, + missedQueue: { + pollIntervalMs: parseInt( + process.env.MISSED_QUEUE_POLL_INTERVAL_MS ?? '30000', + 10, + ), + }, + ai: { + provider: process.env.AI_PROVIDER ?? 'openai', + anthropicApiKey: process.env.ANTHROPIC_API_KEY ?? '', + openaiApiKey: process.env.OPENAI_API_KEY ?? '', + model: process.env.AI_MODEL ?? 'gpt-4o-mini', + }, }); diff --git a/src/exotel/exotel.controller.ts b/src/exotel/exotel.controller.ts index 0422c5b..502c1a9 100644 --- a/src/exotel/exotel.controller.ts +++ b/src/exotel/exotel.controller.ts @@ -5,26 +5,28 @@ import type { ExotelWebhookPayload } from './exotel.types'; @Controller('webhooks/exotel') export class ExotelController { - private readonly logger = new Logger(ExotelController.name); + private readonly logger = new Logger(ExotelController.name); - constructor( - private readonly exotelService: ExotelService, - private readonly callEventsService: CallEventsService, - ) {} + constructor( + private readonly exotelService: ExotelService, + private readonly callEventsService: CallEventsService, + ) {} - @Post('call-status') - @HttpCode(200) - async handleCallStatus(@Body() payload: ExotelWebhookPayload) { - this.logger.log(`Received Exotel webhook: ${payload.event_details?.event_type}`); + @Post('call-status') + @HttpCode(200) + async handleCallStatus(@Body() payload: ExotelWebhookPayload) { + this.logger.log( + `Received Exotel webhook: ${payload.event_details?.event_type}`, + ); - const callEvent = this.exotelService.parseWebhook(payload); + const callEvent = this.exotelService.parseWebhook(payload); - if (callEvent.eventType === 'answered') { - await this.callEventsService.handleIncomingCall(callEvent); - } else if (callEvent.eventType === 'ended') { - await this.callEventsService.handleCallEnded(callEvent); - } - - return { status: 'received' }; + if (callEvent.eventType === 'answered') { + await this.callEventsService.handleIncomingCall(callEvent); + } else if (callEvent.eventType === 'ended') { + await this.callEventsService.handleCallEnded(callEvent); } + + return { status: 'received' }; + } } diff --git a/src/exotel/exotel.module.ts b/src/exotel/exotel.module.ts index 227f0bf..151ebfe 100644 --- a/src/exotel/exotel.module.ts +++ b/src/exotel/exotel.module.ts @@ -4,9 +4,9 @@ import { ExotelController } from './exotel.controller'; import { ExotelService } from './exotel.service'; @Module({ - imports: [CallEventsModule], - controllers: [ExotelController], - providers: [ExotelService], - exports: [ExotelService], + imports: [CallEventsModule], + controllers: [ExotelController], + providers: [ExotelService], + exports: [ExotelService], }) export class ExotelModule {} diff --git a/src/exotel/exotel.service.ts b/src/exotel/exotel.service.ts index 51564a4..88158b4 100644 --- a/src/exotel/exotel.service.ts +++ b/src/exotel/exotel.service.ts @@ -3,29 +3,34 @@ import type { ExotelWebhookPayload, CallEvent } from './exotel.types'; @Injectable() export class ExotelService { - private readonly logger = new Logger(ExotelService.name); + private readonly logger = new Logger(ExotelService.name); - parseWebhook(payload: ExotelWebhookPayload): CallEvent { - const { event_details, call_details } = payload; + parseWebhook(payload: ExotelWebhookPayload): CallEvent { + const { event_details, call_details } = payload; - const eventType = event_details.event_type === 'answered' ? 'answered' - : event_details.event_type === 'terminal' ? 'ended' - : 'ringing'; + const eventType = + event_details.event_type === 'answered' + ? 'answered' + : event_details.event_type === 'terminal' + ? 'ended' + : 'ringing'; - const callEvent: CallEvent = { - exotelCallSid: call_details.call_sid, - eventType, - direction: call_details.direction, - callerPhone: call_details.customer_details?.number ?? '', - agentName: call_details.assigned_agent_details?.name ?? 'Unknown', - agentPhone: call_details.assigned_agent_details?.number ?? '', - duration: call_details.total_talk_time, - recordingUrl: call_details.recordings?.[0]?.url, - callStatus: call_details.call_status, - timestamp: new Date().toISOString(), - }; + const callEvent: CallEvent = { + exotelCallSid: call_details.call_sid, + eventType, + direction: call_details.direction, + callerPhone: call_details.customer_details?.number ?? '', + agentName: call_details.assigned_agent_details?.name ?? 'Unknown', + agentPhone: call_details.assigned_agent_details?.number ?? '', + duration: call_details.total_talk_time, + recordingUrl: call_details.recordings?.[0]?.url, + callStatus: call_details.call_status, + timestamp: new Date().toISOString(), + }; - this.logger.log(`Parsed Exotel event: ${eventType} for call ${call_details.call_sid}`); - return callEvent; - } + this.logger.log( + `Parsed Exotel event: ${eventType} for call ${call_details.call_sid}`, + ); + return callEvent; + } } diff --git a/src/exotel/exotel.types.ts b/src/exotel/exotel.types.ts index f1d4b75..a83b810 100644 --- a/src/exotel/exotel.types.ts +++ b/src/exotel/exotel.types.ts @@ -1,35 +1,35 @@ // Exotel webhook payload (from their API docs) export type ExotelWebhookPayload = { - event_details: { - event_type: 'answered' | 'terminal'; + event_details: { + event_type: 'answered' | 'terminal'; + }; + call_details: { + call_sid: string; + direction: 'inbound' | 'outbound'; + call_status?: string; + total_talk_time?: number; + assigned_agent_details?: { + name: string; + number: string; }; - call_details: { - call_sid: string; - direction: 'inbound' | 'outbound'; - call_status?: string; - total_talk_time?: number; - assigned_agent_details?: { - name: string; - number: string; - }; - customer_details?: { - number: string; - name?: string; - }; - recordings?: { url: string }[]; + customer_details?: { + number: string; + name?: string; }; + recordings?: { url: string }[]; + }; }; // Internal call event (normalized) export type CallEvent = { - exotelCallSid: string; - eventType: 'ringing' | 'answered' | 'ended'; - direction: 'inbound' | 'outbound'; - callerPhone: string; - agentName: string; - agentPhone: string; - duration?: number; - recordingUrl?: string; - callStatus?: string; - timestamp: string; + exotelCallSid: string; + eventType: 'ringing' | 'answered' | 'ended'; + direction: 'inbound' | 'outbound'; + callerPhone: string; + agentName: string; + agentPhone: string; + duration?: number; + recordingUrl?: string; + callStatus?: string; + timestamp: string; }; diff --git a/src/graphql-proxy/graphql-proxy.controller.ts b/src/graphql-proxy/graphql-proxy.controller.ts index a7e3d58..23cdccc 100644 --- a/src/graphql-proxy/graphql-proxy.controller.ts +++ b/src/graphql-proxy/graphql-proxy.controller.ts @@ -1,45 +1,48 @@ -import { Controller, Post, Req, Res, Logger, HttpException } from '@nestjs/common'; +import { + Controller, + Post, + Req, + Res, + Logger, + HttpException, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import type { Request, Response } from 'express'; import axios from 'axios'; @Controller('graphql') export class GraphqlProxyController { - private readonly logger = new Logger(GraphqlProxyController.name); - private readonly graphqlUrl: string; + private readonly logger = new Logger(GraphqlProxyController.name); + private readonly graphqlUrl: string; - constructor(private config: ConfigService) { - this.graphqlUrl = config.get('platform.graphqlUrl')!; + constructor(private config: ConfigService) { + this.graphqlUrl = config.get('platform.graphqlUrl')!; + } + + @Post() + async proxy(@Req() req: Request, @Res() res: Response) { + const authHeader = req.headers.authorization; + + if (!authHeader) { + throw new HttpException('Authorization header required', 401); } - @Post() - async proxy(@Req() req: Request, @Res() res: Response) { - const authHeader = req.headers.authorization; + try { + const response = await axios.post(this.graphqlUrl, req.body, { + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + }); - if (!authHeader) { - throw new HttpException('Authorization header required', 401); - } - - try { - const response = await axios.post( - this.graphqlUrl, - req.body, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': authHeader, - }, - }, - ); - - res.status(response.status).json(response.data); - } catch (error: any) { - if (error.response) { - res.status(error.response.status).json(error.response.data); - } else { - this.logger.error(`GraphQL proxy error: ${error.message}`); - throw new HttpException('Platform unreachable', 503); - } - } + res.status(response.status).json(response.data); + } catch (error: any) { + if (error.response) { + res.status(error.response.status).json(error.response.data); + } else { + this.logger.error(`GraphQL proxy error: ${error.message}`); + throw new HttpException('Platform unreachable', 503); + } } + } } diff --git a/src/graphql-proxy/graphql-proxy.module.ts b/src/graphql-proxy/graphql-proxy.module.ts index a830ebe..adae2c5 100644 --- a/src/graphql-proxy/graphql-proxy.module.ts +++ b/src/graphql-proxy/graphql-proxy.module.ts @@ -2,6 +2,6 @@ import { Module } from '@nestjs/common'; import { GraphqlProxyController } from './graphql-proxy.controller'; @Module({ - controllers: [GraphqlProxyController], + controllers: [GraphqlProxyController], }) export class GraphqlProxyModule {} diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts index 444a71a..123e97a 100644 --- a/src/health/health.controller.ts +++ b/src/health/health.controller.ts @@ -4,35 +4,39 @@ import axios from 'axios'; @Controller('api/health') export class HealthController { - private readonly logger = new Logger(HealthController.name); - private readonly graphqlUrl: string; + private readonly logger = new Logger(HealthController.name); + private readonly graphqlUrl: string; - constructor(private config: ConfigService) { - this.graphqlUrl = config.get('platform.graphqlUrl')!; + constructor(private config: ConfigService) { + this.graphqlUrl = config.get('platform.graphqlUrl')!; + } + + @Get() + async check() { + let platformReachable = false; + let platformLatency = 0; + + try { + const start = Date.now(); + await axios.post( + this.graphqlUrl, + { query: '{ __typename }' }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 5000, + }, + ); + platformLatency = Date.now() - start; + platformReachable = true; + } catch { + platformReachable = false; } - @Get() - async check() { - let platformReachable = false; - let platformLatency = 0; - - try { - const start = Date.now(); - await axios.post(this.graphqlUrl, { query: '{ __typename }' }, { - headers: { 'Content-Type': 'application/json' }, - timeout: 5000, - }); - platformLatency = Date.now() - start; - platformReachable = true; - } catch { - platformReachable = false; - } - - return { - status: platformReachable ? 'ok' : 'degraded', - platform: { reachable: platformReachable, latencyMs: platformLatency }, - ozonetel: { configured: !!process.env.EXOTEL_API_KEY }, - ai: { configured: !!process.env.ANTHROPIC_API_KEY }, - }; - } + return { + status: platformReachable ? 'ok' : 'degraded', + platform: { reachable: platformReachable, latencyMs: platformLatency }, + ozonetel: { configured: !!process.env.EXOTEL_API_KEY }, + ai: { configured: !!process.env.ANTHROPIC_API_KEY }, + }; + } } diff --git a/src/health/health.module.ts b/src/health/health.module.ts index d42135a..7476abe 100644 --- a/src/health/health.module.ts +++ b/src/health/health.module.ts @@ -2,6 +2,6 @@ import { Module } from '@nestjs/common'; import { HealthController } from './health.controller'; @Module({ - controllers: [HealthController], + controllers: [HealthController], }) export class HealthModule {} diff --git a/src/main.ts b/src/main.ts index eb13e25..d68a187 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,32 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - const config = app.get(ConfigService); + const app = await NestFactory.create(AppModule); + const config = app.get(ConfigService); - app.enableCors({ - origin: config.get('corsOrigin'), - credentials: true, - }); + app.enableCors({ + origin: config.get('corsOrigin'), + credentials: true, + }); - const port = config.get('port'); - await app.listen(port); - console.log(`Helix Engage Server running on port ${port}`); + const swaggerConfig = new DocumentBuilder() + .setTitle('Helix Engage Server') + .setDescription( + 'Sidecar API — Ozonetel telephony + FortyTwo platform bridge', + ) + .setVersion('1.0') + .addBearerAuth() + .build(); + + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api/docs', app, document); + + const port = config.get('port'); + await app.listen(port); + console.log(`Helix Engage Server running on port ${port}`); + console.log(`Swagger UI: http://localhost:${port}/api/docs`); } bootstrap(); diff --git a/src/ozonetel/kookoo-ivr.controller.ts b/src/ozonetel/kookoo-ivr.controller.ts index b33e56d..7ad2b86 100644 --- a/src/ozonetel/kookoo-ivr.controller.ts +++ b/src/ozonetel/kookoo-ivr.controller.ts @@ -3,49 +3,53 @@ import { ConfigService } from '@nestjs/config'; @Controller('kookoo') export class KookooIvrController { - private readonly logger = new Logger(KookooIvrController.name); - private readonly sipId: string; - private readonly callerId: string; + private readonly logger = new Logger(KookooIvrController.name); + private readonly sipId: string; + private readonly callerId: string; - constructor(private config: ConfigService) { - this.sipId = process.env.OZONETEL_SIP_ID ?? '523590'; - this.callerId = process.env.OZONETEL_DID ?? '918041763265'; - } + constructor(private config: ConfigService) { + this.sipId = process.env.OZONETEL_SIP_ID ?? '523590'; + this.callerId = process.env.OZONETEL_DID ?? '918041763265'; + } - @Get('ivr') - @Header('Content-Type', 'application/xml') - handleIvr(@Query() query: Record): string { - const event = query.event ?? ''; - const sid = query.sid ?? ''; - const cid = query.cid ?? ''; - const status = query.status ?? ''; + @Get('ivr') + @Header('Content-Type', 'application/xml') + handleIvr(@Query() query: Record): string { + const event = query.event ?? ''; + const sid = query.sid ?? ''; + const cid = query.cid ?? ''; + const status = query.status ?? ''; - this.logger.log(`Kookoo IVR: event=${event} sid=${sid} cid=${cid} status=${status}`); + this.logger.log( + `Kookoo IVR: event=${event} sid=${sid} cid=${cid} status=${status}`, + ); - // New outbound call — customer answered, put them in a conference room - // The room ID is based on the call SID so we can join from the browser - if (event === 'NewCall') { - this.logger.log(`Customer ${cid} answered — dialing DID ${this.callerId} to route to agent`); - return ` + // New outbound call — customer answered, put them in a conference room + // The room ID is based on the call SID so we can join from the browser + if (event === 'NewCall') { + this.logger.log( + `Customer ${cid} answered — dialing DID ${this.callerId} to route to agent`, + ); + return ` ${this.callerId} `; - } + } - // Conference event — user left with # - if (event === 'conference' || event === 'Conference') { - this.logger.log(`Conference event: status=${status}`); - return ` - - -`; - } - - // Dial or Disconnect - this.logger.log(`Call ended: event=${event}`); - return ` + // Conference event — user left with # + if (event === 'conference' || event === 'Conference') { + this.logger.log(`Conference event: status=${status}`); + return ` `; } + + // Dial or Disconnect + this.logger.log(`Call ended: event=${event}`); + return ` + + +`; + } } diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index b8644c1..b7ad7ee 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -1,4 +1,12 @@ -import { Controller, Post, Get, Body, Query, Logger, HttpException } from '@nestjs/common'; +import { + Controller, + Post, + Get, + Body, + Query, + Logger, + HttpException, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { OzonetelAgentService } from './ozonetel-agent.service'; import { MissedQueueService } from '../worklist/missed-queue.service'; @@ -6,328 +14,385 @@ import { PlatformGraphqlService } from '../platform/platform-graphql.service'; @Controller('api/ozonetel') export class OzonetelAgentController { - private readonly logger = new Logger(OzonetelAgentController.name); - private readonly defaultAgentId: string; - private readonly defaultAgentPassword: string; + private readonly logger = new Logger(OzonetelAgentController.name); + private readonly defaultAgentId: string; + private readonly defaultAgentPassword: string; - private readonly defaultSipId: string; + private readonly defaultSipId: string; - constructor( - private readonly ozonetelAgent: OzonetelAgentService, - private readonly config: ConfigService, - private readonly missedQueue: MissedQueueService, - private readonly platform: PlatformGraphqlService, - ) { - this.defaultAgentId = config.get('OZONETEL_AGENT_ID') ?? 'agent3'; - this.defaultAgentPassword = config.get('OZONETEL_AGENT_PASSWORD') ?? ''; - this.defaultSipId = config.get('OZONETEL_SIP_ID') ?? '521814'; + constructor( + private readonly ozonetelAgent: OzonetelAgentService, + private readonly config: ConfigService, + private readonly missedQueue: MissedQueueService, + private readonly platform: PlatformGraphqlService, + ) { + this.defaultAgentId = config.get('OZONETEL_AGENT_ID') ?? 'agent3'; + this.defaultAgentPassword = + config.get('OZONETEL_AGENT_PASSWORD') ?? ''; + this.defaultSipId = config.get('OZONETEL_SIP_ID') ?? '521814'; + } + + @Post('agent-login') + async agentLogin( + @Body() + body: { + agentId: string; + password: string; + phoneNumber: string; + mode?: string; + }, + ) { + this.logger.log(`Agent login request for ${body.agentId}`); + + try { + const result = await this.ozonetelAgent.loginAgent(body); + return result; + } catch (error: any) { + throw new HttpException( + error.response?.data?.message ?? 'Agent login failed', + error.response?.status ?? 500, + ); + } + } + + @Post('agent-logout') + async agentLogout(@Body() body: { agentId: string; password: string }) { + this.logger.log(`Agent logout request for ${body.agentId}`); + + try { + const result = await this.ozonetelAgent.logoutAgent(body); + return result; + } catch (error: any) { + throw new HttpException( + error.response?.data?.message ?? 'Agent logout failed', + error.response?.status ?? 500, + ); + } + } + + @Post('agent-state') + async agentState( + @Body() body: { state: 'Ready' | 'Pause'; pauseReason?: string }, + ) { + if (!body.state) { + throw new HttpException('state required', 400); } - @Post('agent-login') - async agentLogin( - @Body() body: { agentId: string; password: string; phoneNumber: string; mode?: string }, - ) { - this.logger.log(`Agent login request for ${body.agentId}`); + this.logger.log( + `Agent state change: ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? ''})`, + ); - try { - const result = await this.ozonetelAgent.loginAgent(body); - return result; - } catch (error: any) { - throw new HttpException( - error.response?.data?.message ?? 'Agent login failed', - error.response?.status ?? 500, - ); - } + try { + const result = await this.ozonetelAgent.changeAgentState({ + agentId: this.defaultAgentId, + state: body.state, + pauseReason: body.pauseReason, + }); + return result; + } catch (error: any) { + const message = + error.response?.data?.message ?? error.message ?? 'State change failed'; + return { status: 'error', message }; } - @Post('agent-logout') - async agentLogout( - @Body() body: { agentId: string; password: string }, - ) { - this.logger.log(`Agent logout request for ${body.agentId}`); - - try { - const result = await this.ozonetelAgent.logoutAgent(body); - return result; - } catch (error: any) { - throw new HttpException( - error.response?.data?.message ?? 'Agent logout failed', - error.response?.status ?? 500, - ); + // Auto-assign missed call when agent goes Ready + if (body.state === 'Ready') { + try { + const assigned = await this.missedQueue.assignNext(this.defaultAgentId); + if (assigned) { + return { + status: 'ok', + message: `State changed to Ready. Assigned missed call ${assigned.id}`, + assignedCall: assigned, + }; } + } catch (err) { + this.logger.warn(`Auto-assignment on Ready failed: ${err}`); + } + } + } + + @Post('agent-ready') + async agentReady() { + this.logger.log( + `Force ready: logging out and back in agent ${this.defaultAgentId}`, + ); + + try { + await this.ozonetelAgent.logoutAgent({ + agentId: this.defaultAgentId, + password: this.defaultAgentPassword, + }); + const result = await this.ozonetelAgent.loginAgent({ + agentId: this.defaultAgentId, + password: this.defaultAgentPassword, + phoneNumber: this.defaultSipId, + mode: 'blended', + }); + return result; + } catch (error: any) { + const message = + error.response?.data?.message ?? error.message ?? 'Force ready failed'; + this.logger.error(`Force ready failed: ${message}`); + throw new HttpException(message, error.response?.status ?? 502); + } + } + + @Post('dispose') + async dispose( + @Body() + body: { + ucid: string; + disposition: string; + callerPhone?: string; + direction?: string; + durationSec?: number; + leadId?: string; + notes?: string; + missedCallId?: string; + }, + ) { + if (!body.ucid || !body.disposition) { + throw new HttpException('ucid and disposition required', 400); } - @Post('agent-state') - async agentState( - @Body() body: { state: 'Ready' | 'Pause'; pauseReason?: string }, - ) { - if (!body.state) { - throw new HttpException('state required', 400); - } + this.logger.log( + `Dispose: ucid=${body.ucid} disposition=${body.disposition}`, + ); - this.logger.log(`Agent state change: ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? ''})`); + const ozonetelDisposition = this.mapToOzonetelDisposition(body.disposition); - try { - const result = await this.ozonetelAgent.changeAgentState({ - agentId: this.defaultAgentId, - state: body.state, - pauseReason: body.pauseReason, - }); - return result; - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'State change failed'; - return { status: 'error', message }; - } - - // Auto-assign missed call when agent goes Ready - if (body.state === 'Ready') { - try { - const assigned = await this.missedQueue.assignNext(this.defaultAgentId); - if (assigned) { - return { status: 'ok', message: `State changed to Ready. Assigned missed call ${assigned.id}`, assignedCall: assigned }; - } - } catch (err) { - this.logger.warn(`Auto-assignment on Ready failed: ${err}`); - } - } + try { + const result = await this.ozonetelAgent.setDisposition({ + agentId: this.defaultAgentId, + ucid: body.ucid, + disposition: ozonetelDisposition, + }); + } catch (error: any) { + const message = + error.response?.data?.message ?? error.message ?? 'Disposition failed'; + this.logger.error(`Dispose failed: ${message}`); } - @Post('agent-ready') - async agentReady() { - this.logger.log(`Force ready: logging out and back in agent ${this.defaultAgentId}`); - + // Handle missed call callback status update + if (body.missedCallId) { + const statusMap: Record = { + APPOINTMENT_BOOKED: 'CALLBACK_COMPLETED', + INFO_PROVIDED: 'CALLBACK_COMPLETED', + FOLLOW_UP_SCHEDULED: 'CALLBACK_COMPLETED', + CALLBACK_REQUESTED: 'CALLBACK_COMPLETED', + WRONG_NUMBER: 'WRONG_NUMBER', + }; + const newStatus = statusMap[body.disposition]; + if (newStatus) { try { - await this.ozonetelAgent.logoutAgent({ - agentId: this.defaultAgentId, - password: this.defaultAgentPassword, - }); - const result = await this.ozonetelAgent.loginAgent({ - agentId: this.defaultAgentId, - password: this.defaultAgentPassword, - phoneNumber: this.defaultSipId, - mode: 'blended', - }); - return result; - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'Force ready failed'; - this.logger.error(`Force ready failed: ${message}`); - throw new HttpException(message, error.response?.status ?? 502); - } - } - - @Post('dispose') - async dispose( - @Body() body: { - ucid: string; - disposition: string; - callerPhone?: string; - direction?: string; - durationSec?: number; - leadId?: string; - notes?: string; - missedCallId?: string; - }, - ) { - if (!body.ucid || !body.disposition) { - throw new HttpException('ucid and disposition required', 400); - } - - this.logger.log(`Dispose: ucid=${body.ucid} disposition=${body.disposition}`); - - const ozonetelDisposition = this.mapToOzonetelDisposition(body.disposition); - - try { - const result = await this.ozonetelAgent.setDisposition({ - agentId: this.defaultAgentId, - ucid: body.ucid, - disposition: ozonetelDisposition, - }); - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'Disposition failed'; - this.logger.error(`Dispose failed: ${message}`); - } - - // Handle missed call callback status update - if (body.missedCallId) { - const statusMap: Record = { - APPOINTMENT_BOOKED: 'CALLBACK_COMPLETED', - INFO_PROVIDED: 'CALLBACK_COMPLETED', - FOLLOW_UP_SCHEDULED: 'CALLBACK_COMPLETED', - CALLBACK_REQUESTED: 'CALLBACK_COMPLETED', - WRONG_NUMBER: 'WRONG_NUMBER', - }; - const newStatus = statusMap[body.disposition]; - if (newStatus) { - try { - await this.platform.query( - `mutation { updateCall(id: "${body.missedCallId}", data: { callbackstatus: ${newStatus} }) { id } }`, - ); - } catch (err) { - this.logger.warn(`Failed to update missed call status: ${err}`); - } - } - } - - // Auto-assign next missed call to this agent - try { - await this.missedQueue.assignNext(this.defaultAgentId); + await this.platform.query( + `mutation { updateCall(id: "${body.missedCallId}", data: { callbackstatus: ${newStatus} }) { id } }`, + ); } catch (err) { - this.logger.warn(`Auto-assignment after dispose failed: ${err}`); + this.logger.warn(`Failed to update missed call status: ${err}`); } - - return { status: 'ok' }; + } } - @Post('dial') - async dial( - @Body() body: { phoneNumber: string; campaignName?: string; leadId?: string }, - ) { - if (!body.phoneNumber) { - throw new HttpException('phoneNumber required', 400); - } - - const campaignName = body.campaignName ?? process.env.OZONETEL_CAMPAIGN_NAME ?? 'Inbound_918041763265'; - - this.logger.log(`Manual dial: ${body.phoneNumber} campaign=${campaignName} (lead: ${body.leadId ?? 'none'})`); - - try { - const result = await this.ozonetelAgent.manualDial({ - agentId: this.defaultAgentId, - campaignName, - customerNumber: body.phoneNumber, - }); - return result; - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'Dial failed'; - throw new HttpException(message, error.response?.status ?? 502); - } + // Auto-assign next missed call to this agent + try { + await this.missedQueue.assignNext(this.defaultAgentId); + } catch (err) { + this.logger.warn(`Auto-assignment after dispose failed: ${err}`); } - @Post('call-control') - async callControl( - @Body() body: { - action: 'CONFERENCE' | 'HOLD' | 'UNHOLD' | 'MUTE' | 'UNMUTE' | 'KICK_CALL'; - ucid: string; - conferenceNumber?: string; - }, - ) { - if (!body.action || !body.ucid) { - throw new HttpException('action and ucid required', 400); - } - if (body.action === 'CONFERENCE' && !body.conferenceNumber) { - throw new HttpException('conferenceNumber required for CONFERENCE action', 400); - } + return { status: 'ok' }; + } - this.logger.log(`Call control: ${body.action} ucid=${body.ucid}`); - - try { - const result = await this.ozonetelAgent.callControl(body); - return result; - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'Call control failed'; - throw new HttpException(message, error.response?.status ?? 502); - } + @Post('dial') + async dial( + @Body() + body: { + phoneNumber: string; + campaignName?: string; + leadId?: string; + }, + ) { + if (!body.phoneNumber) { + throw new HttpException('phoneNumber required', 400); } - @Post('recording') - async recording( - @Body() body: { ucid: string; action: 'pause' | 'unPause' }, - ) { - if (!body.ucid || !body.action) { - throw new HttpException('ucid and action required', 400); - } + const campaignName = + body.campaignName ?? + process.env.OZONETEL_CAMPAIGN_NAME ?? + 'Inbound_918041763265'; - try { - const result = await this.ozonetelAgent.pauseRecording(body); - return result; - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'Recording control failed'; - throw new HttpException(message, error.response?.status ?? 502); - } + this.logger.log( + `Manual dial: ${body.phoneNumber} campaign=${campaignName} (lead: ${body.leadId ?? 'none'})`, + ); + + try { + const result = await this.ozonetelAgent.manualDial({ + agentId: this.defaultAgentId, + campaignName, + customerNumber: body.phoneNumber, + }); + return result; + } catch (error: any) { + const message = + error.response?.data?.message ?? error.message ?? 'Dial failed'; + throw new HttpException(message, error.response?.status ?? 502); + } + } + + @Post('call-control') + async callControl( + @Body() + body: { + action: + | 'CONFERENCE' + | 'HOLD' + | 'UNHOLD' + | 'MUTE' + | 'UNMUTE' + | 'KICK_CALL'; + ucid: string; + conferenceNumber?: string; + }, + ) { + if (!body.action || !body.ucid) { + throw new HttpException('action and ucid required', 400); + } + if (body.action === 'CONFERENCE' && !body.conferenceNumber) { + throw new HttpException( + 'conferenceNumber required for CONFERENCE action', + 400, + ); } - @Get('missed-calls') - async missedCalls() { - const result = await this.ozonetelAgent.getAbandonCalls(); - return result; + this.logger.log(`Call control: ${body.action} ucid=${body.ucid}`); + + try { + const result = await this.ozonetelAgent.callControl(body); + return result; + } catch (error: any) { + const message = + error.response?.data?.message ?? error.message ?? 'Call control failed'; + throw new HttpException(message, error.response?.status ?? 502); + } + } + + @Post('recording') + async recording(@Body() body: { ucid: string; action: 'pause' | 'unPause' }) { + if (!body.ucid || !body.action) { + throw new HttpException('ucid and action required', 400); } - @Get('call-history') - async callHistory( - @Query('date') date?: string, - @Query('status') status?: string, - @Query('callType') callType?: string, - ) { - const targetDate = date ?? new Date().toISOString().split('T')[0]; - this.logger.log(`Call history: date=${targetDate} status=${status ?? 'all'} type=${callType ?? 'all'}`); + try { + const result = await this.ozonetelAgent.pauseRecording(body); + return result; + } catch (error: any) { + const message = + error.response?.data?.message ?? + error.message ?? + 'Recording control failed'; + throw new HttpException(message, error.response?.status ?? 502); + } + } - const result = await this.ozonetelAgent.fetchCDR({ - date: targetDate, - status, - callType, - }); - return result; + @Get('missed-calls') + async missedCalls() { + const result = await this.ozonetelAgent.getAbandonCalls(); + return result; + } + + @Get('call-history') + async callHistory( + @Query('date') date?: string, + @Query('status') status?: string, + @Query('callType') callType?: string, + ) { + const targetDate = date ?? new Date().toISOString().split('T')[0]; + this.logger.log( + `Call history: date=${targetDate} status=${status ?? 'all'} type=${callType ?? 'all'}`, + ); + + const result = await this.ozonetelAgent.fetchCDR({ + date: targetDate, + status, + callType, + }); + return result; + } + + @Get('performance') + async performance(@Query('date') date?: string) { + const targetDate = date ?? new Date().toISOString().split('T')[0]; + this.logger.log( + `Performance: date=${targetDate} agent=${this.defaultAgentId}`, + ); + + const [cdr, summary, aht] = await Promise.all([ + this.ozonetelAgent.fetchCDR({ date: targetDate }), + this.ozonetelAgent.getAgentSummary(this.defaultAgentId, targetDate), + this.ozonetelAgent.getAHT(this.defaultAgentId), + ]); + + const totalCalls = cdr.length; + const inbound = cdr.filter((c: any) => c.Type === 'InBound').length; + const outbound = cdr.filter( + (c: any) => c.Type === 'Manual' || c.Type === 'Progressive', + ).length; + const answered = cdr.filter((c: any) => c.Status === 'Answered').length; + const missed = cdr.filter( + (c: any) => c.Status === 'Unanswered' || c.Status === 'NotAnswered', + ).length; + + const talkTimes = cdr + .filter((c: any) => c.TalkTime && c.TalkTime !== '00:00:00') + .map((c: any) => { + const parts = c.TalkTime.split(':').map(Number); + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + }); + const avgTalkTimeSec = + talkTimes.length > 0 + ? Math.round( + talkTimes.reduce((a: number, b: number) => a + b, 0) / + talkTimes.length, + ) + : 0; + + const dispositions: Record = {}; + for (const c of cdr) { + const d = (c as any).Disposition || 'No Disposition'; + dispositions[d] = (dispositions[d] ?? 0) + 1; } - @Get('performance') - async performance(@Query('date') date?: string) { - const targetDate = date ?? new Date().toISOString().split('T')[0]; - this.logger.log(`Performance: date=${targetDate} agent=${this.defaultAgentId}`); + const appointmentsBooked = cdr.filter((c: any) => + c.Disposition?.toLowerCase().includes('appointment'), + ).length; - const [cdr, summary, aht] = await Promise.all([ - this.ozonetelAgent.fetchCDR({ date: targetDate }), - this.ozonetelAgent.getAgentSummary(this.defaultAgentId, targetDate), - this.ozonetelAgent.getAHT(this.defaultAgentId), - ]); + return { + date: targetDate, + calls: { total: totalCalls, inbound, outbound, answered, missed }, + avgTalkTimeSec, + avgHandlingTime: aht, + conversionRate: + totalCalls > 0 + ? Math.round((appointmentsBooked / totalCalls) * 100) + : 0, + appointmentsBooked, + timeUtilization: summary, + dispositions, + }; + } - const totalCalls = cdr.length; - const inbound = cdr.filter((c: any) => c.Type === 'InBound').length; - const outbound = cdr.filter((c: any) => c.Type === 'Manual' || c.Type === 'Progressive').length; - const answered = cdr.filter((c: any) => c.Status === 'Answered').length; - const missed = cdr.filter((c: any) => c.Status === 'Unanswered' || c.Status === 'NotAnswered').length; - - const talkTimes = cdr - .filter((c: any) => c.TalkTime && c.TalkTime !== '00:00:00') - .map((c: any) => { - const parts = c.TalkTime.split(':').map(Number); - return parts[0] * 3600 + parts[1] * 60 + parts[2]; - }); - const avgTalkTimeSec = talkTimes.length > 0 - ? Math.round(talkTimes.reduce((a: number, b: number) => a + b, 0) / talkTimes.length) - : 0; - - const dispositions: Record = {}; - for (const c of cdr) { - const d = (c as any).Disposition || 'No Disposition'; - dispositions[d] = (dispositions[d] ?? 0) + 1; - } - - const appointmentsBooked = cdr.filter((c: any) => - c.Disposition?.toLowerCase().includes('appointment'), - ).length; - - return { - date: targetDate, - calls: { total: totalCalls, inbound, outbound, answered, missed }, - avgTalkTimeSec, - avgHandlingTime: aht, - conversionRate: totalCalls > 0 ? Math.round((appointmentsBooked / totalCalls) * 100) : 0, - appointmentsBooked, - timeUtilization: summary, - dispositions, - }; - } - - private mapToOzonetelDisposition(disposition: string): string { - // Campaign only has 'General Enquiry' configured currently - const map: Record = { - 'APPOINTMENT_BOOKED': 'General Enquiry', - 'FOLLOW_UP_SCHEDULED': 'General Enquiry', - 'INFO_PROVIDED': 'General Enquiry', - 'NO_ANSWER': 'General Enquiry', - 'WRONG_NUMBER': 'General Enquiry', - 'CALLBACK_REQUESTED': 'General Enquiry', - }; - return map[disposition] ?? 'General Enquiry'; - } + private mapToOzonetelDisposition(disposition: string): string { + // Campaign only has 'General Enquiry' configured currently + const map: Record = { + APPOINTMENT_BOOKED: 'General Enquiry', + FOLLOW_UP_SCHEDULED: 'General Enquiry', + INFO_PROVIDED: 'General Enquiry', + NO_ANSWER: 'General Enquiry', + WRONG_NUMBER: 'General Enquiry', + CALLBACK_REQUESTED: 'General Enquiry', + }; + return map[disposition] ?? 'General Enquiry'; + } } diff --git a/src/ozonetel/ozonetel-agent.module.ts b/src/ozonetel/ozonetel-agent.module.ts index e6a8857..4300afe 100644 --- a/src/ozonetel/ozonetel-agent.module.ts +++ b/src/ozonetel/ozonetel-agent.module.ts @@ -6,9 +6,9 @@ import { WorklistModule } from '../worklist/worklist.module'; import { PlatformModule } from '../platform/platform.module'; @Module({ - imports: [PlatformModule, forwardRef(() => WorklistModule)], - controllers: [OzonetelAgentController, KookooIvrController], - providers: [OzonetelAgentService], - exports: [OzonetelAgentService], + imports: [PlatformModule, forwardRef(() => WorklistModule)], + controllers: [OzonetelAgentController, KookooIvrController], + providers: [OzonetelAgentService], + exports: [OzonetelAgentService], }) export class OzonetelAgentModule {} diff --git a/src/ozonetel/ozonetel-agent.service.ts b/src/ozonetel/ozonetel-agent.service.ts index 8267b1d..866cf84 100644 --- a/src/ozonetel/ozonetel-agent.service.ts +++ b/src/ozonetel/ozonetel-agent.service.ts @@ -4,463 +4,530 @@ import axios from 'axios'; @Injectable() export class OzonetelAgentService { - private readonly logger = new Logger(OzonetelAgentService.name); - private readonly apiDomain: string; - private readonly apiKey: string; - private readonly accountId: string; - private cachedToken: string | null = null; - private tokenExpiry: number = 0; + private readonly logger = new Logger(OzonetelAgentService.name); + private readonly apiDomain: string; + private readonly apiKey: string; + private readonly accountId: string; + private cachedToken: string | null = null; + private tokenExpiry: number = 0; - constructor(private config: ConfigService) { - this.apiDomain = config.get('exotel.subdomain') ?? 'in1-ccaas-api.ozonetel.com'; - this.apiKey = config.get('exotel.apiKey') ?? ''; - this.accountId = config.get('exotel.accountSid') ?? ''; + constructor(private config: ConfigService) { + this.apiDomain = + config.get('exotel.subdomain') ?? 'in1-ccaas-api.ozonetel.com'; + this.apiKey = config.get('exotel.apiKey') ?? ''; + this.accountId = config.get('exotel.accountSid') ?? ''; + } + + private async getToken(): Promise { + if (this.cachedToken && Date.now() < this.tokenExpiry) { + return this.cachedToken; } - private async getToken(): Promise { - if (this.cachedToken && Date.now() < this.tokenExpiry) { - return this.cachedToken; - } + const url = `https://${this.apiDomain}/ca_apis/CAToken/generateToken`; + this.logger.log('Generating CloudAgent API token'); - const url = `https://${this.apiDomain}/ca_apis/CAToken/generateToken`; - this.logger.log('Generating CloudAgent API token'); + const response = await axios.post( + url, + { userName: this.accountId }, + { + headers: { apiKey: this.apiKey, 'Content-Type': 'application/json' }, + }, + ); - const response = await axios.post(url, { userName: this.accountId }, { - headers: { apiKey: this.apiKey, 'Content-Type': 'application/json' }, - }); - - const data = response.data; - if (data.token) { - this.cachedToken = data.token; - this.tokenExpiry = Date.now() + 55 * 60 * 1000; - this.logger.log('CloudAgent token generated successfully'); - return data.token; - } - - throw new Error(data.message ?? 'Token generation failed'); + const data = response.data; + if (data.token) { + this.cachedToken = data.token; + this.tokenExpiry = Date.now() + 55 * 60 * 1000; + this.logger.log('CloudAgent token generated successfully'); + return data.token; } - async loginAgent(params: { - agentId: string; - password: string; - phoneNumber: string; - mode?: string; - }): Promise<{ status: string; message: string }> { - const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`; + throw new Error(data.message ?? 'Token generation failed'); + } - this.logger.log(`Logging in agent ${params.agentId} with phone ${params.phoneNumber}`); + async loginAgent(params: { + agentId: string; + password: string; + phoneNumber: string; + mode?: string; + }): Promise<{ status: string; message: string }> { + const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`; - try { - const response = await axios.post( - url, - new URLSearchParams({ - userName: this.accountId, - apiKey: this.apiKey, - phoneNumber: params.phoneNumber, - action: 'login', - mode: params.mode ?? 'blended', - state: 'Ready', - }).toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - auth: { - username: params.agentId, - password: params.password, - }, - }, - ); + this.logger.log( + `Logging in agent ${params.agentId} with phone ${params.phoneNumber}`, + ); - const data = response.data; + try { + const response = await axios.post( + url, + new URLSearchParams({ + userName: this.accountId, + apiKey: this.apiKey, + phoneNumber: params.phoneNumber, + action: 'login', + mode: params.mode ?? 'blended', + state: 'Ready', + }).toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth: { + username: params.agentId, + password: params.password, + }, + }, + ); - // "already logged in" is not a real error — treat as success - if (data.status === 'error' && data.message?.includes('already logged in')) { - this.logger.log(`Agent ${params.agentId} already logged in — treating as success`); - return { status: 'success', message: data.message }; - } + const data = response.data; - this.logger.log(`Agent login response: ${JSON.stringify(data)}`); - return data; - } catch (error: any) { - this.logger.error(`Agent login failed: ${error.message}`); - throw error; - } + // "already logged in" is not a real error — treat as success + if ( + data.status === 'error' && + data.message?.includes('already logged in') + ) { + this.logger.log( + `Agent ${params.agentId} already logged in — treating as success`, + ); + return { status: 'success', message: data.message }; + } + + this.logger.log(`Agent login response: ${JSON.stringify(data)}`); + return data; + } catch (error: any) { + this.logger.error(`Agent login failed: ${error.message}`); + throw error; } + } - async manualDial(params: { - agentId: string; - campaignName: string; - customerNumber: string; - }): Promise<{ status: string; ucid?: string; message?: string }> { - const url = `https://${this.apiDomain}/ca_apis/AgentManualDial`; + async manualDial(params: { + agentId: string; + campaignName: string; + customerNumber: string; + }): Promise<{ status: string; ucid?: string; message?: string }> { + const url = `https://${this.apiDomain}/ca_apis/AgentManualDial`; - this.logger.log(`Manual dial: agent=${params.agentId} campaign=${params.campaignName} number=${params.customerNumber}`); + this.logger.log( + `Manual dial: agent=${params.agentId} campaign=${params.campaignName} number=${params.customerNumber}`, + ); - try { - const token = await this.getToken(); - const response = await axios.post(url, { - userName: this.accountId, - agentID: params.agentId, - campaignName: params.campaignName, - customerNumber: params.customerNumber, - UCID: 'true', - }, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); + try { + const token = await this.getToken(); + const response = await axios.post( + url, + { + userName: this.accountId, + agentID: params.agentId, + campaignName: params.campaignName, + customerNumber: params.customerNumber, + UCID: 'true', + }, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }, + ); - this.logger.log(`Manual dial response: ${JSON.stringify(response.data)}`); - return response.data; - } catch (error: any) { - const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; - this.logger.error(`Manual dial failed: ${error.message} ${responseData}`); - throw error; - } + this.logger.log(`Manual dial response: ${JSON.stringify(response.data)}`); + return response.data; + } catch (error: any) { + const responseData = error?.response?.data + ? JSON.stringify(error.response.data) + : ''; + this.logger.error(`Manual dial failed: ${error.message} ${responseData}`); + throw error; } + } - async changeAgentState(params: { - agentId: string; - state: 'Ready' | 'Pause'; - pauseReason?: string; - }): Promise<{ status: string; message: string }> { - const url = `https://${this.apiDomain}/ca_apis/changeAgentState`; + async changeAgentState(params: { + agentId: string; + state: 'Ready' | 'Pause'; + pauseReason?: string; + }): Promise<{ status: string; message: string }> { + const url = `https://${this.apiDomain}/ca_apis/changeAgentState`; - this.logger.log(`Changing agent ${params.agentId} state to ${params.state}`); + this.logger.log( + `Changing agent ${params.agentId} state to ${params.state}`, + ); - try { - const body: Record = { - userName: this.accountId, - agentId: params.agentId, - state: params.state, - }; - if (params.pauseReason) { - body.pauseReason = params.pauseReason; - } + try { + const body: Record = { + userName: this.accountId, + agentId: params.agentId, + state: params.state, + }; + if (params.pauseReason) { + body.pauseReason = params.pauseReason; + } - const token = await this.getToken(); - const response = await axios.post(url, body, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); + const token = await this.getToken(); + const response = await axios.post(url, body, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); - this.logger.log(`Change agent state response: ${JSON.stringify(response.data)}`); - return response.data; - } catch (error: any) { - const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; - this.logger.error(`Change agent state failed: ${error.message} ${responseData}`); - throw error; - } + this.logger.log( + `Change agent state response: ${JSON.stringify(response.data)}`, + ); + return response.data; + } catch (error: any) { + const responseData = error?.response?.data + ? JSON.stringify(error.response.data) + : ''; + this.logger.error( + `Change agent state failed: ${error.message} ${responseData}`, + ); + throw error; } + } - async setDisposition(params: { - agentId: string; - ucid: string; - disposition: string; - }): Promise<{ status: string; message?: string; details?: string }> { - const url = `https://${this.apiDomain}/ca_apis/DispositionAPIV2`; - const did = process.env.OZONETEL_DID ?? '918041763265'; + async setDisposition(params: { + agentId: string; + ucid: string; + disposition: string; + }): Promise<{ status: string; message?: string; details?: string }> { + const url = `https://${this.apiDomain}/ca_apis/DispositionAPIV2`; + const did = process.env.OZONETEL_DID ?? '918041763265'; - this.logger.log(`Set disposition: agent=${params.agentId} ucid=${params.ucid} disposition=${params.disposition}`); + this.logger.log( + `Set disposition: agent=${params.agentId} ucid=${params.ucid} disposition=${params.disposition}`, + ); - try { - const token = await this.getToken(); - const response = await axios.post(url, { - userName: this.accountId, - agentID: params.agentId, - did, - ucid: params.ucid, - action: 'Set', - disposition: params.disposition, - autoRelease: 'true', - }, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); + try { + const token = await this.getToken(); + const response = await axios.post( + url, + { + userName: this.accountId, + agentID: params.agentId, + did, + ucid: params.ucid, + action: 'Set', + disposition: params.disposition, + autoRelease: 'true', + }, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }, + ); - this.logger.log(`Set disposition response: ${JSON.stringify(response.data)}`); - return response.data; - } catch (error: any) { - const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; - this.logger.error(`Set disposition failed: ${error.message} ${responseData}`); - throw error; - } + this.logger.log( + `Set disposition response: ${JSON.stringify(response.data)}`, + ); + return response.data; + } catch (error: any) { + const responseData = error?.response?.data + ? JSON.stringify(error.response.data) + : ''; + this.logger.error( + `Set disposition failed: ${error.message} ${responseData}`, + ); + throw error; } + } - async callControl(params: { - action: 'CONFERENCE' | 'HOLD' | 'UNHOLD' | 'MUTE' | 'UNMUTE' | 'KICK_CALL'; - ucid: string; - conferenceNumber?: string; - }): Promise<{ status: string; message: string; ucid?: string }> { - const url = `https://${this.apiDomain}/ca_apis/CallControl_V4`; - const did = process.env.OZONETEL_DID ?? '918041763265'; - const agentPhoneName = process.env.OZONETEL_SIP_ID ?? '523590'; + async callControl(params: { + action: 'CONFERENCE' | 'HOLD' | 'UNHOLD' | 'MUTE' | 'UNMUTE' | 'KICK_CALL'; + ucid: string; + conferenceNumber?: string; + }): Promise<{ status: string; message: string; ucid?: string }> { + const url = `https://${this.apiDomain}/ca_apis/CallControl_V4`; + const did = process.env.OZONETEL_DID ?? '918041763265'; + const agentPhoneName = process.env.OZONETEL_SIP_ID ?? '523590'; - this.logger.log(`Call control: action=${params.action} ucid=${params.ucid} conference=${params.conferenceNumber ?? 'none'}`); + this.logger.log( + `Call control: action=${params.action} ucid=${params.ucid} conference=${params.conferenceNumber ?? 'none'}`, + ); - try { - const token = await this.getToken(); - const body: Record = { - userName: this.accountId, - action: params.action, - ucid: params.ucid, - did, - agentPhoneName, - }; - if (params.conferenceNumber) { - body.conferenceNumber = params.conferenceNumber; - } + try { + const token = await this.getToken(); + const body: Record = { + userName: this.accountId, + action: params.action, + ucid: params.ucid, + did, + agentPhoneName, + }; + if (params.conferenceNumber) { + body.conferenceNumber = params.conferenceNumber; + } - const response = await axios.post(url, body, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); + const response = await axios.post(url, body, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); - this.logger.log(`Call control response: ${JSON.stringify(response.data)}`); - return response.data; - } catch (error: any) { - const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; - this.logger.error(`Call control failed: ${error.message} ${responseData}`); - throw error; - } + this.logger.log( + `Call control response: ${JSON.stringify(response.data)}`, + ); + return response.data; + } catch (error: any) { + const responseData = error?.response?.data + ? JSON.stringify(error.response.data) + : ''; + this.logger.error( + `Call control failed: ${error.message} ${responseData}`, + ); + throw error; } + } - async pauseRecording(params: { - ucid: string; - action: 'pause' | 'unPause'; - }): Promise<{ status: string; message: string }> { - const url = `https://${this.apiDomain}/CAServices/Call/Record.php`; + async pauseRecording(params: { + ucid: string; + action: 'pause' | 'unPause'; + }): Promise<{ status: string; message: string }> { + const url = `https://${this.apiDomain}/CAServices/Call/Record.php`; - this.logger.log(`Recording ${params.action}: ucid=${params.ucid}`); + this.logger.log(`Recording ${params.action}: ucid=${params.ucid}`); - try { - const response = await axios.get(url, { - params: { - userName: this.accountId, - apiKey: this.apiKey, - action: params.action, - ucid: params.ucid, - }, - }); + try { + const response = await axios.get(url, { + params: { + userName: this.accountId, + apiKey: this.apiKey, + action: params.action, + ucid: params.ucid, + }, + }); - this.logger.log(`Recording control response: ${JSON.stringify(response.data)}`); - return response.data; - } catch (error: any) { - const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; - this.logger.error(`Recording control failed: ${error.message} ${responseData}`); - throw error; - } + this.logger.log( + `Recording control response: ${JSON.stringify(response.data)}`, + ); + return response.data; + } catch (error: any) { + const responseData = error?.response?.data + ? JSON.stringify(error.response.data) + : ''; + this.logger.error( + `Recording control failed: ${error.message} ${responseData}`, + ); + throw error; } + } - async getAbandonCalls(params?: { - fromTime?: string; - toTime?: string; - campaignName?: string; - }): Promise> { - const url = `https://${this.apiDomain}/ca_apis/abandonCalls`; + async getAbandonCalls(params?: { + fromTime?: string; + toTime?: string; + campaignName?: string; + }): Promise< + Array<{ + monitorUCID: string; + type: string; + status: string; + campaign: string; + callerID: string; + did: string; + agentID: string; + agent: string; + hangupBy: string; + callTime: string; + }> + > { + const url = `https://${this.apiDomain}/ca_apis/abandonCalls`; - this.logger.log('Fetching abandon calls'); + this.logger.log('Fetching abandon calls'); - try { - const token = await this.getToken(); - const body: Record = { userName: this.accountId }; - if (params?.fromTime) body.fromTime = params.fromTime; - if (params?.toTime) body.toTime = params.toTime; - if (params?.campaignName) body.campaignName = params.campaignName; + try { + const token = await this.getToken(); + const body: Record = { userName: this.accountId }; + if (params?.fromTime) body.fromTime = params.fromTime; + if (params?.toTime) body.toTime = params.toTime; + if (params?.campaignName) body.campaignName = params.campaignName; - const response = await axios({ - method: 'GET', - url, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - data: JSON.stringify(body), - }); + const response = await axios({ + method: 'GET', + url, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: JSON.stringify(body), + }); - const data = response.data; - this.logger.log(`Abandon calls response: ${JSON.stringify(data).substring(0, 200)}`); - if (data.status === 'success' && Array.isArray(data.message)) { - return data.message; - } - return []; - } catch (error: any) { - this.logger.error(`Abandon calls failed: ${error.message}`); - return []; - } + const data = response.data; + this.logger.log( + `Abandon calls response: ${JSON.stringify(data).substring(0, 200)}`, + ); + if (data.status === 'success' && Array.isArray(data.message)) { + return data.message; + } + return []; + } catch (error: any) { + this.logger.error(`Abandon calls failed: ${error.message}`); + return []; } + } - async fetchCDR(params: { - date: string; // YYYY-MM-DD - campaignName?: string; - status?: string; - callType?: string; - }): Promise>> { - const url = `https://${this.apiDomain}/ca_reports/fetchCDRDetails`; + async fetchCDR(params: { + date: string; // YYYY-MM-DD + campaignName?: string; + status?: string; + callType?: string; + }): Promise>> { + const url = `https://${this.apiDomain}/ca_reports/fetchCDRDetails`; - this.logger.log(`Fetch CDR: date=${params.date}`); + this.logger.log(`Fetch CDR: date=${params.date}`); - try { - const token = await this.getToken(); - const body: Record = { - userName: this.accountId, - fromDate: `${params.date} 00:00:00`, - toDate: `${params.date} 23:59:59`, - }; - if (params.campaignName) body.campaignName = params.campaignName; - if (params.status) body.status = params.status; - if (params.callType) body.callType = params.callType; + try { + const token = await this.getToken(); + const body: Record = { + userName: this.accountId, + fromDate: `${params.date} 00:00:00`, + toDate: `${params.date} 23:59:59`, + }; + if (params.campaignName) body.campaignName = params.campaignName; + if (params.status) body.status = params.status; + if (params.callType) body.callType = params.callType; - const response = await axios({ - method: 'GET', - url, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - data: JSON.stringify(body), - }); + const response = await axios({ + method: 'GET', + url, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: JSON.stringify(body), + }); - const data = response.data; - if (data.status === 'success' && Array.isArray(data.details)) { - return data.details; - } - return []; - } catch (error: any) { - const responseData = error?.response?.data ? JSON.stringify(error.response.data) : ''; - this.logger.error(`Fetch CDR failed: ${error.message} ${responseData}`); - return []; - } + const data = response.data; + if (data.status === 'success' && Array.isArray(data.details)) { + return data.details; + } + return []; + } catch (error: any) { + const responseData = error?.response?.data + ? JSON.stringify(error.response.data) + : ''; + this.logger.error(`Fetch CDR failed: ${error.message} ${responseData}`); + return []; } + } - async getAgentSummary(agentId: string, date: string): Promise<{ - totalLoginDuration: string; - totalBusyTime: string; - totalIdleTime: string; - totalPauseTime: string; - totalWrapupTime: string; - totalDialTime: string; - } | null> { - const url = `https://${this.apiDomain}/ca_reports/summaryReport`; + async getAgentSummary( + agentId: string, + date: string, + ): Promise<{ + totalLoginDuration: string; + totalBusyTime: string; + totalIdleTime: string; + totalPauseTime: string; + totalWrapupTime: string; + totalDialTime: string; + } | null> { + const url = `https://${this.apiDomain}/ca_reports/summaryReport`; - try { - const token = await this.getToken(); - const response = await axios({ - method: 'GET', - url, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - data: JSON.stringify({ - userName: this.accountId, - agentId, - fromDate: `${date} 00:00:00`, - toDate: `${date} 23:59:59`, - }), - }); + try { + const token = await this.getToken(); + const response = await axios({ + method: 'GET', + url, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: JSON.stringify({ + userName: this.accountId, + agentId, + fromDate: `${date} 00:00:00`, + toDate: `${date} 23:59:59`, + }), + }); - const data = response.data; - if (data.status === 'success' && data.message) { - const record = Array.isArray(data.message) ? data.message[0] : data.message; - return { - totalLoginDuration: record.TotalLoginDuration ?? '00:00:00', - totalBusyTime: record.TotalBusyTime ?? '00:00:00', - totalIdleTime: record.TotalIdleTime ?? '00:00:00', - totalPauseTime: record.TotalPauseTime ?? '00:00:00', - totalWrapupTime: record.TotalWrapupTime ?? '00:00:00', - totalDialTime: record.TotalDialTime ?? '00:00:00', - }; - } - return null; - } catch (error: any) { - this.logger.error(`Agent summary failed: ${error.message}`); - return null; - } + const data = response.data; + if (data.status === 'success' && data.message) { + const record = Array.isArray(data.message) + ? data.message[0] + : data.message; + return { + totalLoginDuration: record.TotalLoginDuration ?? '00:00:00', + totalBusyTime: record.TotalBusyTime ?? '00:00:00', + totalIdleTime: record.TotalIdleTime ?? '00:00:00', + totalPauseTime: record.TotalPauseTime ?? '00:00:00', + totalWrapupTime: record.TotalWrapupTime ?? '00:00:00', + totalDialTime: record.TotalDialTime ?? '00:00:00', + }; + } + return null; + } catch (error: any) { + this.logger.error(`Agent summary failed: ${error.message}`); + return null; } + } - async getAHT(agentId: string): Promise { - const url = `https://${this.apiDomain}/ca_apis/aht`; + async getAHT(agentId: string): Promise { + const url = `https://${this.apiDomain}/ca_apis/aht`; - try { - const token = await this.getToken(); - const response = await axios({ - method: 'GET', - url, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - data: JSON.stringify({ - userName: this.accountId, - agentId, - }), - }); + try { + const token = await this.getToken(); + const response = await axios({ + method: 'GET', + url, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: JSON.stringify({ + userName: this.accountId, + agentId, + }), + }); - const data = response.data; - if (data.status === 'success') { - return data.AHT ?? '00:00:00'; - } - return '00:00:00'; - } catch (error: any) { - this.logger.error(`AHT failed: ${error.message}`); - return '00:00:00'; - } + const data = response.data; + if (data.status === 'success') { + return data.AHT ?? '00:00:00'; + } + return '00:00:00'; + } catch (error: any) { + this.logger.error(`AHT failed: ${error.message}`); + return '00:00:00'; } + } - async logoutAgent(params: { - agentId: string; - password: string; - }): Promise<{ status: string; message: string }> { - const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`; + async logoutAgent(params: { + agentId: string; + password: string; + }): Promise<{ status: string; message: string }> { + const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`; - this.logger.log(`Logging out agent ${params.agentId}`); + this.logger.log(`Logging out agent ${params.agentId}`); - try { - const response = await axios.post( - url, - new URLSearchParams({ - userName: this.accountId, - apiKey: this.apiKey, - action: 'logout', - mode: 'blended', - state: 'Ready', - }).toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - auth: { - username: params.agentId, - password: params.password, - }, - }, - ); + try { + const response = await axios.post( + url, + new URLSearchParams({ + userName: this.accountId, + apiKey: this.apiKey, + action: 'logout', + mode: 'blended', + state: 'Ready', + }).toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + auth: { + username: params.agentId, + password: params.password, + }, + }, + ); - this.logger.log(`Agent logout response: ${JSON.stringify(response.data)}`); - return response.data; - } catch (error: any) { - this.logger.error(`Agent logout failed: ${error.message}`); - throw error; - } + this.logger.log( + `Agent logout response: ${JSON.stringify(response.data)}`, + ); + return response.data; + } catch (error: any) { + this.logger.error(`Agent logout failed: ${error.message}`); + throw error; } + } } diff --git a/src/platform/platform-graphql.service.ts b/src/platform/platform-graphql.service.ts index cc8389a..ea3bfe2 100644 --- a/src/platform/platform-graphql.service.ts +++ b/src/platform/platform-graphql.service.ts @@ -1,48 +1,58 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; -import type { LeadNode, LeadActivityNode, CreateCallInput, CreateLeadActivityInput, UpdateLeadInput } from './platform.types'; +import type { + LeadNode, + LeadActivityNode, + CreateCallInput, + CreateLeadActivityInput, + UpdateLeadInput, +} from './platform.types'; @Injectable() export class PlatformGraphqlService { - private readonly graphqlUrl: string; - private readonly apiKey: string; + private readonly graphqlUrl: string; + private readonly apiKey: string; - constructor(private config: ConfigService) { - this.graphqlUrl = config.get('platform.graphqlUrl')!; - this.apiKey = config.get('platform.apiKey')!; + constructor(private config: ConfigService) { + this.graphqlUrl = config.get('platform.graphqlUrl')!; + this.apiKey = config.get('platform.apiKey')!; + } + + // Server-to-server query using API key + async query(query: string, variables?: Record): Promise { + return this.queryWithAuth(query, variables, `Bearer ${this.apiKey}`); + } + + // Query using a passed-through auth header (user JWT) + async queryWithAuth( + query: string, + variables: Record | undefined, + authHeader: string, + ): Promise { + const response = await axios.post( + this.graphqlUrl, + { query, variables }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + }, + ); + + if (response.data.errors) { + throw new Error(`GraphQL error: ${JSON.stringify(response.data.errors)}`); } - // Server-to-server query using API key - async query(query: string, variables?: Record): Promise { - return this.queryWithAuth(query, variables, `Bearer ${this.apiKey}`); - } + return response.data.data; + } - // Query using a passed-through auth header (user JWT) - async queryWithAuth(query: string, variables: Record | undefined, authHeader: string): Promise { - const response = await axios.post( - this.graphqlUrl, - { query, variables }, - { - headers: { - 'Content-Type': 'application/json', - 'Authorization': authHeader, - }, - }, - ); - - if (response.data.errors) { - throw new Error(`GraphQL error: ${JSON.stringify(response.data.errors)}`); - } - - return response.data.data; - } - - async findLeadByPhone(phone: string): Promise { - // Note: The exact filter syntax for PHONES fields depends on the platform - // This queries leads and filters client-side by phone number - const data = await this.query<{ leads: { edges: { node: LeadNode }[] } }>( - `query FindLeads($first: Int) { + async findLeadByPhone(phone: string): Promise { + // Note: The exact filter syntax for PHONES fields depends on the platform + // This queries leads and filters client-side by phone number + const data = await this.query<{ leads: { edges: { node: LeadNode }[] } }>( + `query FindLeads($first: Int) { leads(first: $first, orderBy: { createdAt: DescNullsLast }) { edges { node { @@ -58,20 +68,26 @@ export class PlatformGraphqlService { } } }`, - { first: 100 }, + { first: 100 }, + ); + + // Client-side phone matching (strip non-digits for comparison) + const normalizedPhone = phone.replace(/\D/g, ''); + return ( + data.leads.edges.find((edge) => { + const leadPhones = edge.node.contactPhone ?? []; + return leadPhones.some( + (p) => + p.number.replace(/\D/g, '').endsWith(normalizedPhone) || + normalizedPhone.endsWith(p.number.replace(/\D/g, '')), ); + })?.node ?? null + ); + } - // Client-side phone matching (strip non-digits for comparison) - const normalizedPhone = phone.replace(/\D/g, ''); - return data.leads.edges.find(edge => { - const leadPhones = edge.node.contactPhone ?? []; - return leadPhones.some(p => p.number.replace(/\D/g, '').endsWith(normalizedPhone) || normalizedPhone.endsWith(p.number.replace(/\D/g, ''))); - })?.node ?? null; - } - - async findLeadById(id: string): Promise { - const data = await this.query<{ lead: LeadNode }>( - `query FindLead($id: ID!) { + async findLeadById(id: string): Promise { + const data = await this.query<{ lead: LeadNode }>( + `query FindLead($id: ID!) { lead(id: $id) { id createdAt contactName { firstName lastName } @@ -83,51 +99,58 @@ export class PlatformGraphqlService { aiSummary aiSuggestedAction } }`, - { id }, - ); - return data.lead; - } + { id }, + ); + return data.lead; + } - async updateLead(id: string, input: UpdateLeadInput): Promise { - const data = await this.query<{ updateLead: LeadNode }>( - `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { + async updateLead(id: string, input: UpdateLeadInput): Promise { + const data = await this.query<{ updateLead: LeadNode }>( + `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id leadStatus aiSummary aiSuggestedAction } }`, - { id, data: input }, - ); - return data.updateLead; - } + { id, data: input }, + ); + return data.updateLead; + } - async createCall(input: CreateCallInput): Promise<{ id: string }> { - const data = await this.query<{ createCall: { id: string } }>( - `mutation CreateCall($data: CallCreateInput!) { + async createCall(input: CreateCallInput): Promise<{ id: string }> { + const data = await this.query<{ createCall: { id: string } }>( + `mutation CreateCall($data: CallCreateInput!) { createCall(data: $data) { id } }`, - { data: input }, - ); - return data.createCall; - } + { data: input }, + ); + return data.createCall; + } - async createLeadActivity(input: CreateLeadActivityInput): Promise<{ id: string }> { - const data = await this.query<{ createLeadActivity: { id: string } }>( - `mutation CreateLeadActivity($data: LeadActivityCreateInput!) { + async createLeadActivity( + input: CreateLeadActivityInput, + ): Promise<{ id: string }> { + const data = await this.query<{ createLeadActivity: { id: string } }>( + `mutation CreateLeadActivity($data: LeadActivityCreateInput!) { createLeadActivity(data: $data) { id } }`, - { data: input }, - ); - return data.createLeadActivity; - } + { data: input }, + ); + return data.createLeadActivity; + } - // --- Token passthrough versions (for user-driven requests) --- + // --- Token passthrough versions (for user-driven requests) --- - async findLeadByPhoneWithToken(phone: string, authHeader: string): Promise { - const normalizedPhone = phone.replace(/\D/g, ''); - const last10 = normalizedPhone.slice(-10); + async findLeadByPhoneWithToken( + phone: string, + authHeader: string, + ): Promise { + const normalizedPhone = phone.replace(/\D/g, ''); + const last10 = normalizedPhone.slice(-10); - const data = await this.queryWithAuth<{ leads: { edges: { node: LeadNode }[] } }>( - `query FindLeads($first: Int) { + const data = await this.queryWithAuth<{ + leads: { edges: { node: LeadNode }[] }; + }>( + `query FindLeads($first: Int) { leads(first: $first, orderBy: [{ createdAt: DescNullsLast }]) { edges { node { @@ -143,28 +166,43 @@ export class PlatformGraphqlService { } } }`, - { first: 200 }, - authHeader, - ); + { first: 200 }, + authHeader, + ); - // Client-side phone matching - return data.leads.edges.find(edge => { - const phones = edge.node.contactPhone ?? []; - if (Array.isArray(phones)) { - return phones.some((p: any) => { - const num = (p.number ?? p.primaryPhoneNumber ?? '').replace(/\D/g, ''); - return num.endsWith(last10) || last10.endsWith(num); - }); - } - // Handle single phone object - const num = ((phones as any).primaryPhoneNumber ?? (phones as any).number ?? '').replace(/\D/g, ''); + // Client-side phone matching + return ( + data.leads.edges.find((edge) => { + const phones = edge.node.contactPhone ?? []; + if (Array.isArray(phones)) { + return phones.some((p: any) => { + const num = (p.number ?? p.primaryPhoneNumber ?? '').replace( + /\D/g, + '', + ); return num.endsWith(last10) || last10.endsWith(num); - })?.node ?? null; - } + }); + } + // Handle single phone object + const num = ( + (phones as any).primaryPhoneNumber ?? + (phones as any).number ?? + '' + ).replace(/\D/g, ''); + return num.endsWith(last10) || last10.endsWith(num); + })?.node ?? null + ); + } - async getLeadActivitiesWithToken(leadId: string, authHeader: string, limit = 5): Promise { - const data = await this.queryWithAuth<{ leadActivities: { edges: { node: LeadActivityNode }[] } }>( - `query GetLeadActivities($filter: LeadActivityFilterInput, $first: Int) { + async getLeadActivitiesWithToken( + leadId: string, + authHeader: string, + limit = 5, + ): Promise { + const data = await this.queryWithAuth<{ + leadActivities: { edges: { node: LeadActivityNode }[] }; + }>( + `query GetLeadActivities($filter: LeadActivityFilterInput, $first: Int) { leadActivities(filter: $filter, first: $first, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node { @@ -173,30 +211,39 @@ export class PlatformGraphqlService { } } }`, - { filter: { leadId: { eq: leadId } }, first: limit }, - authHeader, - ); - return data.leadActivities.edges.map(e => e.node); - } + { filter: { leadId: { eq: leadId } }, first: limit }, + authHeader, + ); + return data.leadActivities.edges.map((e) => e.node); + } - async updateLeadWithToken(id: string, input: UpdateLeadInput, authHeader: string): Promise { - const data = await this.queryWithAuth<{ updateLead: LeadNode }>( - `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { + async updateLeadWithToken( + id: string, + input: UpdateLeadInput, + authHeader: string, + ): Promise { + const data = await this.queryWithAuth<{ updateLead: LeadNode }>( + `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id leadStatus aiSummary aiSuggestedAction } }`, - { id, data: input }, - authHeader, - ); - return data.updateLead; - } + { id, data: input }, + authHeader, + ); + return data.updateLead; + } - // --- Server-to-server versions (for webhooks, background jobs) --- + // --- Server-to-server versions (for webhooks, background jobs) --- - async getLeadActivities(leadId: string, limit = 3): Promise { - const data = await this.query<{ leadActivities: { edges: { node: LeadActivityNode }[] } }>( - `query GetLeadActivities($filter: LeadActivityFilterInput, $first: Int) { + async getLeadActivities( + leadId: string, + limit = 3, + ): Promise { + const data = await this.query<{ + leadActivities: { edges: { node: LeadActivityNode }[] }; + }>( + `query GetLeadActivities($filter: LeadActivityFilterInput, $first: Int) { leadActivities(filter: $filter, first: $first, orderBy: { occurredAt: DescNullsLast }) { edges { node { @@ -205,8 +252,8 @@ export class PlatformGraphqlService { } } }`, - { filter: { leadId: { eq: leadId } }, first: limit }, - ); - return data.leadActivities.edges.map(e => e.node); - } + { filter: { leadId: { eq: leadId } }, first: limit }, + ); + return data.leadActivities.edges.map((e) => e.node); + } } diff --git a/src/platform/platform.module.ts b/src/platform/platform.module.ts index 08164f9..6156ed0 100644 --- a/src/platform/platform.module.ts +++ b/src/platform/platform.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { PlatformGraphqlService } from './platform-graphql.service'; @Module({ - providers: [PlatformGraphqlService], - exports: [PlatformGraphqlService], + providers: [PlatformGraphqlService], + exports: [PlatformGraphqlService], }) export class PlatformModule {} diff --git a/src/platform/platform.types.ts b/src/platform/platform.types.ts index 490d5f3..2558850 100644 --- a/src/platform/platform.types.ts +++ b/src/platform/platform.types.ts @@ -1,71 +1,71 @@ export type LeadNode = { - id: string; - createdAt: string; - contactName: { firstName: string; lastName: string } | null; - contactPhone: { number: string; callingCode: string }[] | null; - contactEmail: { address: string }[] | null; - leadSource: string | null; - leadStatus: string | null; - interestedService: string | null; - assignedAgent: string | null; - campaignId: string | null; - adId: string | null; - contactAttempts: number | null; - spamScore: number | null; - isSpam: boolean | null; - aiSummary: string | null; - aiSuggestedAction: string | null; + id: string; + createdAt: string; + contactName: { firstName: string; lastName: string } | null; + contactPhone: { number: string; callingCode: string }[] | null; + contactEmail: { address: string }[] | null; + leadSource: string | null; + leadStatus: string | null; + interestedService: string | null; + assignedAgent: string | null; + campaignId: string | null; + adId: string | null; + contactAttempts: number | null; + spamScore: number | null; + isSpam: boolean | null; + aiSummary: string | null; + aiSuggestedAction: string | null; }; export type LeadActivityNode = { - id: string; - activityType: string | null; - summary: string | null; - occurredAt: string | null; - performedBy: string | null; - channel: string | null; + id: string; + activityType: string | null; + summary: string | null; + occurredAt: string | null; + performedBy: string | null; + channel: string | null; }; export type CallNode = { - id: string; - callDirection: string | null; - callStatus: string | null; - disposition: string | null; - agentName: string | null; - startedAt: string | null; - endedAt: string | null; - durationSeconds: number | null; - leadId: string | null; + id: string; + callDirection: string | null; + callStatus: string | null; + disposition: string | null; + agentName: string | null; + startedAt: string | null; + endedAt: string | null; + durationSeconds: number | null; + leadId: string | null; }; export type CreateCallInput = { - callDirection: string; - callStatus: string; - callerNumber?: { number: string; callingCode: string }[]; - agentName?: string; - startedAt?: string; - endedAt?: string; - durationSeconds?: number; - disposition?: string; - callNotes?: string; - leadId?: string; + callDirection: string; + callStatus: string; + callerNumber?: { number: string; callingCode: string }[]; + agentName?: string; + startedAt?: string; + endedAt?: string; + durationSeconds?: number; + disposition?: string; + callNotes?: string; + leadId?: string; }; export type CreateLeadActivityInput = { - activityType: string; - summary: string; - occurredAt: string; - performedBy: string; - channel: string; - durationSeconds?: number; - outcome?: string; - leadId: string; + activityType: string; + summary: string; + occurredAt: string; + performedBy: string; + channel: string; + durationSeconds?: number; + outcome?: string; + leadId: string; }; export type UpdateLeadInput = { - leadStatus?: string; - lastContactedAt?: string; - aiSummary?: string; - aiSuggestedAction?: string; - contactAttempts?: number; + leadStatus?: string; + lastContactedAt?: string; + aiSummary?: string; + aiSuggestedAction?: string; + contactAttempts?: number; }; diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index 9944c51..acfe6f4 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -4,91 +4,113 @@ import { PlatformGraphqlService } from '../platform/platform-graphql.service'; @Controller('api/search') export class SearchController { - private readonly logger = new Logger(SearchController.name); - private readonly platformApiKey: string; + private readonly logger = new Logger(SearchController.name); + private readonly platformApiKey: string; - constructor( - private readonly platform: PlatformGraphqlService, - private readonly config: ConfigService, - ) { - this.platformApiKey = config.get('platform.apiKey') ?? ''; + constructor( + private readonly platform: PlatformGraphqlService, + private readonly config: ConfigService, + ) { + this.platformApiKey = config.get('platform.apiKey') ?? ''; + } + + @Get() + async search(@Query('q') query?: string) { + if (!query || query.length < 2) { + return { leads: [], patients: [], appointments: [] }; } - @Get() - async search(@Query('q') query?: string) { - if (!query || query.length < 2) { - return { leads: [], patients: [], appointments: [] }; - } + const authHeader = this.platformApiKey + ? `Bearer ${this.platformApiKey}` + : ''; + if (!authHeader) { + return { leads: [], patients: [], appointments: [] }; + } - const authHeader = this.platformApiKey ? `Bearer ${this.platformApiKey}` : ''; - if (!authHeader) { - return { leads: [], patients: [], appointments: [] }; - } + this.logger.log(`Search: "${query}"`); - this.logger.log(`Search: "${query}"`); - - // Fetch all three in parallel, filter client-side for flexible matching - try { - const [leadsResult, patientsResult, appointmentsResult] = await Promise.all([ - this.platform.queryWithAuth( - `{ leads(first: 50) { edges { node { + // Fetch all three in parallel, filter client-side for flexible matching + try { + const [leadsResult, patientsResult, appointmentsResult] = + await Promise.all([ + this.platform + .queryWithAuth( + `{ leads(first: 50) { edges { node { id name contactName { firstName lastName } contactPhone { primaryPhoneNumber } source status interestedService } } } }`, - undefined, authHeader, - ).catch(() => ({ leads: { edges: [] } })), + undefined, + authHeader, + ) + .catch(() => ({ leads: { edges: [] } })), - this.platform.queryWithAuth( - `{ patients(first: 50) { edges { node { + this.platform + .queryWithAuth( + `{ patients(first: 50) { edges { node { id name fullName { firstName lastName } phones { primaryPhoneNumber } gender dateOfBirth } } } }`, - undefined, authHeader, - ).catch(() => ({ patients: { edges: [] } })), + undefined, + authHeader, + ) + .catch(() => ({ patients: { edges: [] } })), - this.platform.queryWithAuth( - `{ appointments(first: 50, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { + this.platform + .queryWithAuth( + `{ appointments(first: 50, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { id scheduledAt doctorName department appointmentStatus patientId } } } }`, - undefined, authHeader, - ).catch(() => ({ appointments: { edges: [] } })), - ]); + undefined, + authHeader, + ) + .catch(() => ({ appointments: { edges: [] } })), + ]); - const q = query.toLowerCase(); + const q = query.toLowerCase(); - const leads = (leadsResult.leads?.edges ?? []) - .map((e: any) => e.node) - .filter((l: any) => { - const name = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); - const phone = l.contactPhone?.primaryPhoneNumber ?? ''; - return name.includes(q) || phone.includes(q) || (l.name ?? '').toLowerCase().includes(q); - }) - .slice(0, 5); + const leads = (leadsResult.leads?.edges ?? []) + .map((e: any) => e.node) + .filter((l: any) => { + const name = + `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); + const phone = l.contactPhone?.primaryPhoneNumber ?? ''; + return ( + name.includes(q) || + phone.includes(q) || + (l.name ?? '').toLowerCase().includes(q) + ); + }) + .slice(0, 5); - const patients = (patientsResult.patients?.edges ?? []) - .map((e: any) => e.node) - .filter((p: any) => { - const name = `${p.fullName?.firstName ?? ''} ${p.fullName?.lastName ?? ''}`.toLowerCase(); - const phone = p.phones?.primaryPhoneNumber ?? ''; - return name.includes(q) || phone.includes(q) || (p.name ?? '').toLowerCase().includes(q); - }) - .slice(0, 5); + const patients = (patientsResult.patients?.edges ?? []) + .map((e: any) => e.node) + .filter((p: any) => { + const name = + `${p.fullName?.firstName ?? ''} ${p.fullName?.lastName ?? ''}`.toLowerCase(); + const phone = p.phones?.primaryPhoneNumber ?? ''; + return ( + name.includes(q) || + phone.includes(q) || + (p.name ?? '').toLowerCase().includes(q) + ); + }) + .slice(0, 5); - const appointments = (appointmentsResult.appointments?.edges ?? []) - .map((e: any) => e.node) - .filter((a: any) => { - const doctor = (a.doctorName ?? '').toLowerCase(); - const dept = (a.department ?? '').toLowerCase(); - return doctor.includes(q) || dept.includes(q); - }) - .slice(0, 5); + const appointments = (appointmentsResult.appointments?.edges ?? []) + .map((e: any) => e.node) + .filter((a: any) => { + const doctor = (a.doctorName ?? '').toLowerCase(); + const dept = (a.department ?? '').toLowerCase(); + return doctor.includes(q) || dept.includes(q); + }) + .slice(0, 5); - return { leads, patients, appointments }; - } catch (err: any) { - this.logger.error(`Search failed: ${err.message}`); - return { leads: [], patients: [], appointments: [] }; - } + return { leads, patients, appointments }; + } catch (err: any) { + this.logger.error(`Search failed: ${err.message}`); + return { leads: [], patients: [], appointments: [] }; } + } } diff --git a/src/search/search.module.ts b/src/search/search.module.ts index 59cdd81..590b751 100644 --- a/src/search/search.module.ts +++ b/src/search/search.module.ts @@ -3,7 +3,7 @@ import { SearchController } from './search.controller'; import { PlatformModule } from '../platform/platform.module'; @Module({ - imports: [PlatformModule], - controllers: [SearchController], + imports: [PlatformModule], + controllers: [SearchController], }) export class SearchModule {} diff --git a/src/worklist/kookoo-callback.controller.ts b/src/worklist/kookoo-callback.controller.ts index 9b7b60e..a90a224 100644 --- a/src/worklist/kookoo-callback.controller.ts +++ b/src/worklist/kookoo-callback.controller.ts @@ -4,89 +4,111 @@ import { PlatformGraphqlService } from '../platform/platform-graphql.service'; @Controller('webhooks/kookoo') export class KookooCallbackController { - private readonly logger = new Logger(KookooCallbackController.name); - private readonly apiKey: string; + private readonly logger = new Logger(KookooCallbackController.name); + private readonly apiKey: string; - constructor( - private readonly platform: PlatformGraphqlService, - private readonly config: ConfigService, - ) { - this.apiKey = config.get('platform.apiKey') ?? ''; + constructor( + private readonly platform: PlatformGraphqlService, + private readonly config: ConfigService, + ) { + this.apiKey = config.get('platform.apiKey') ?? ''; + } + + @Post('callback') + async handleCallback( + @Body() body: Record, + @Query() query: Record, + ) { + // Kookoo sends params as both query and body + const params = { ...query, ...body }; + this.logger.log( + `Kookoo callback: sid=${params.sid} status=${params.status} phone=${params.phone_no} duration=${params.duration}`, + ); + + const phoneNumber = (params.phone_no ?? '').replace(/^\+?91/, ''); + const status = params.status ?? 'unknown'; + const duration = parseInt(params.duration ?? '0', 10); + const callerId = params.caller_id ?? ''; + const startTime = params.start_time ?? null; + const endTime = params.end_time ?? null; + const sid = params.sid ?? null; + + if (!phoneNumber) { + return { received: true, processed: false }; } - @Post('callback') - async handleCallback(@Body() body: Record, @Query() query: Record) { - // Kookoo sends params as both query and body - const params = { ...query, ...body }; - this.logger.log(`Kookoo callback: sid=${params.sid} status=${params.status} phone=${params.phone_no} duration=${params.duration}`); + const callStatus = status === 'answered' ? 'COMPLETED' : 'MISSED'; + const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : ''; - const phoneNumber = (params.phone_no ?? '').replace(/^\+?91/, ''); - const status = params.status ?? 'unknown'; - const duration = parseInt(params.duration ?? '0', 10); - const callerId = params.caller_id ?? ''; - const startTime = params.start_time ?? null; - const endTime = params.end_time ?? null; - const sid = params.sid ?? null; - - if (!phoneNumber) { - return { received: true, processed: false }; - } - - const callStatus = status === 'answered' ? 'COMPLETED' : 'MISSED'; - const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : ''; - - if (!authHeader) { - this.logger.warn('No PLATFORM_API_KEY — cannot write call records'); - return { received: true, processed: false }; - } - - try { - // Create call record - const callResult = await this.platform.queryWithAuth( - `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, - { - data: { - name: `Outbound — ${phoneNumber}`, - direction: 'OUTBOUND', - callStatus, - callerNumber: { primaryPhoneNumber: `+91${phoneNumber}` }, - startedAt: startTime ? new Date(startTime).toISOString() : new Date().toISOString(), - endedAt: endTime ? new Date(endTime).toISOString() : null, - durationSec: duration, - }, - }, - authHeader, - ); - - this.logger.log(`Created outbound call record: ${callResult.createCall.id} (${callStatus}, ${duration}s)`); - - // Try to match to a lead - const leadResult = await this.platform.queryWithAuth( - `{ leads(first: 50) { edges { node { id name contactPhone { primaryPhoneNumber } } } } }`, - undefined, - authHeader, - ); - const leads = leadResult.leads.edges.map((e: any) => e.node); - const cleanPhone = phoneNumber.replace(/\D/g, ''); - const matchedLead = leads.find((l: any) => { - const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, ''); - return lp.endsWith(cleanPhone) || cleanPhone.endsWith(lp); - }); - - if (matchedLead) { - await this.platform.queryWithAuth( - `mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`, - { id: callResult.createCall.id, data: { leadId: matchedLead.id } }, - authHeader, - ); - this.logger.log(`Linked call to lead ${matchedLead.id} (${matchedLead.name})`); - } - - return { received: true, processed: true, callId: callResult.createCall.id }; - } catch (err: any) { - const responseData = err?.response?.data ? JSON.stringify(err.response.data) : ''; - this.logger.error(`Kookoo callback processing failed: ${err.message} ${responseData}`); - return { received: true, processed: false }; - } + if (!authHeader) { + this.logger.warn('No PLATFORM_API_KEY — cannot write call records'); + return { received: true, processed: false }; } + + try { + // Create call record + const callResult = await this.platform.queryWithAuth( + `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, + { + data: { + name: `Outbound — ${phoneNumber}`, + direction: 'OUTBOUND', + callStatus, + callerNumber: { primaryPhoneNumber: `+91${phoneNumber}` }, + startedAt: startTime + ? new Date(startTime).toISOString() + : new Date().toISOString(), + endedAt: endTime ? new Date(endTime).toISOString() : null, + durationSec: duration, + }, + }, + authHeader, + ); + + this.logger.log( + `Created outbound call record: ${callResult.createCall.id} (${callStatus}, ${duration}s)`, + ); + + // Try to match to a lead + const leadResult = await this.platform.queryWithAuth( + `{ leads(first: 50) { edges { node { id name contactPhone { primaryPhoneNumber } } } } }`, + undefined, + authHeader, + ); + const leads = leadResult.leads.edges.map((e: any) => e.node); + const cleanPhone = phoneNumber.replace(/\D/g, ''); + const matchedLead = leads.find((l: any) => { + const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace( + /\D/g, + '', + ); + return lp.endsWith(cleanPhone) || cleanPhone.endsWith(lp); + }); + + if (matchedLead) { + await this.platform.queryWithAuth( + `mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`, + { id: callResult.createCall.id, data: { leadId: matchedLead.id } }, + authHeader, + ); + this.logger.log( + `Linked call to lead ${matchedLead.id} (${matchedLead.name})`, + ); + } + + return { + received: true, + processed: true, + callId: callResult.createCall.id, + }; + } catch (err: any) { + const responseData = err?.response?.data + ? JSON.stringify(err.response.data) + : ''; + this.logger.error( + `Kookoo callback processing failed: ${err.message} ${responseData}`, + ); + return { received: true, processed: false }; + } + } } diff --git a/src/worklist/missed-call-webhook.controller.ts b/src/worklist/missed-call-webhook.controller.ts index 6fa3bb5..34055da 100644 --- a/src/worklist/missed-call-webhook.controller.ts +++ b/src/worklist/missed-call-webhook.controller.ts @@ -4,224 +4,280 @@ import { ConfigService } from '@nestjs/config'; @Controller('webhooks/ozonetel') export class MissedCallWebhookController { - private readonly logger = new Logger(MissedCallWebhookController.name); - private readonly apiKey: string; + private readonly logger = new Logger(MissedCallWebhookController.name); + private readonly apiKey: string; - constructor( - private readonly platform: PlatformGraphqlService, - private readonly config: ConfigService, - ) { - this.apiKey = config.get('platform.apiKey') ?? ''; + constructor( + private readonly platform: PlatformGraphqlService, + private readonly config: ConfigService, + ) { + this.apiKey = config.get('platform.apiKey') ?? ''; + } + + @Post('missed-call') + async handleCallWebhook(@Body() body: Record) { + // Ozonetel sends the payload as a JSON string inside a "data" field + let payload: Record; + try { + payload = typeof body.data === 'string' ? JSON.parse(body.data) : body; + } catch { + payload = body; } - @Post('missed-call') - async handleCallWebhook(@Body() body: Record) { - // Ozonetel sends the payload as a JSON string inside a "data" field - let payload: Record; - try { - payload = typeof body.data === 'string' ? JSON.parse(body.data) : body; - } catch { - payload = body; - } + this.logger.log( + `Call webhook: ${payload.CallerID} | ${payload.Status} | ${payload.Type}`, + ); - this.logger.log(`Call webhook: ${payload.CallerID} | ${payload.Status} | ${payload.Type}`); + const callerPhone = (payload.CallerID ?? '').replace(/^\+?91/, ''); + const status = payload.Status; // NotAnswered, Answered, Abandoned + const type = payload.Type; // InBound, OutBound + const startTime = payload.StartTime; + const endTime = payload.EndTime; + const duration = this.parseDuration(payload.CallDuration ?? '00:00:00'); + const agentName = payload.AgentName ?? null; + const recordingUrl = payload.AudioFile ?? null; + const ucid = payload.monitorUCID ?? null; + const disposition = payload.Disposition ?? null; + const hangupBy = payload.HangupBy ?? null; - const callerPhone = (payload.CallerID ?? '').replace(/^\+?91/, ''); - const status = payload.Status; // NotAnswered, Answered, Abandoned - const type = payload.Type; // InBound, OutBound - const startTime = payload.StartTime; - const endTime = payload.EndTime; - const duration = this.parseDuration(payload.CallDuration ?? '00:00:00'); - const agentName = payload.AgentName ?? null; - const recordingUrl = payload.AudioFile ?? null; - const ucid = payload.monitorUCID ?? null; - const disposition = payload.Disposition ?? null; - const hangupBy = payload.HangupBy ?? null; - - if (!callerPhone) { - this.logger.warn('No caller phone in webhook — skipping'); - return { received: true, processed: false }; - } - - // Determine call status for our platform - const callStatus = status === 'Answered' ? 'COMPLETED' : 'MISSED'; - const direction = type === 'InBound' ? 'INBOUND' : 'OUTBOUND'; - - // Use API key auth for server-to-server writes - const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : ''; - if (!authHeader) { - this.logger.warn('No PLATFORM_API_KEY configured — cannot write call records'); - return { received: true, processed: false }; - } - - try { - // Step 1: Create call record - const callId = await this.createCall({ - callerPhone, - direction, - callStatus, - agentName, - startTime, - endTime, - duration, - recordingUrl, - disposition, - ucid, - }, authHeader); - - this.logger.log(`Created call record: ${callId} (${callStatus})`); - - // Step 2: Find matching lead by phone number - const lead = await this.findLeadByPhone(callerPhone, authHeader); - - if (lead) { - // Step 3: Link call to lead - await this.updateCall(callId, { leadId: lead.id }, authHeader); - - // Step 4: Create lead activity - const summary = callStatus === 'MISSED' - ? `Missed inbound call from ${callerPhone} (${duration}s, ${hangupBy ?? 'unknown'})` - : `Inbound call from ${callerPhone} — ${duration}s, ${disposition || 'no disposition'}`; - - await this.createLeadActivity({ - leadId: lead.id, - activityType: callStatus === 'MISSED' ? 'CALL_RECEIVED' : 'CALL_RECEIVED', - summary, - channel: 'PHONE', - performedBy: agentName ?? 'System', - durationSeconds: duration, - outcome: callStatus === 'MISSED' ? 'NO_ANSWER' : 'SUCCESSFUL', - }, authHeader); - - // Step 5: Update lead contact timestamps - await this.updateLead(lead.id, { - lastContacted: startTime ? new Date(startTime).toISOString() : new Date().toISOString(), - contactAttempts: (lead.contactAttempts ?? 0) + 1, - }, authHeader); - - this.logger.log(`Linked call to lead ${lead.id} (${lead.name}), activity created`); - } else { - this.logger.log(`No matching lead for ${callerPhone} — call record created without lead link`); - } - - return { received: true, processed: true, callId, leadId: lead?.id ?? null }; - } catch (err: any) { - const responseData = err?.response?.data ? JSON.stringify(err.response.data) : ''; - this.logger.error(`Webhook processing failed: ${err.message} ${responseData}`); - return { received: true, processed: false, error: String(err) }; - } + if (!callerPhone) { + this.logger.warn('No caller phone in webhook — skipping'); + return { received: true, processed: false }; } - private async createCall(data: { - callerPhone: string; - direction: string; - callStatus: string; - agentName: string | null; - startTime: string | null; - endTime: string | null; - duration: number; - recordingUrl: string | null; - disposition: string | null; - ucid: string | null; - }, authHeader: string): Promise { - const callData: Record = { - name: `${data.direction === 'INBOUND' ? 'Inbound' : 'Outbound'} — ${data.callerPhone}`, - direction: data.direction, - callStatus: data.callStatus, - callerNumber: { primaryPhoneNumber: `+91${data.callerPhone}` }, - agentName: data.agentName, - startedAt: data.startTime ? new Date(data.startTime).toISOString() : null, - endedAt: data.endTime ? new Date(data.endTime).toISOString() : null, - durationSec: data.duration, - disposition: this.mapDisposition(data.disposition), - }; - if (data.recordingUrl) { - callData.recording = { primaryLinkUrl: data.recordingUrl, primaryLinkLabel: 'Recording' }; - } + // Determine call status for our platform + const callStatus = status === 'Answered' ? 'COMPLETED' : 'MISSED'; + const direction = type === 'InBound' ? 'INBOUND' : 'OUTBOUND'; - const result = await this.platform.queryWithAuth( - `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, - { data: callData }, - authHeader, + // Use API key auth for server-to-server writes + const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : ''; + if (!authHeader) { + this.logger.warn( + 'No PLATFORM_API_KEY configured — cannot write call records', + ); + return { received: true, processed: false }; + } + + try { + // Step 1: Create call record + const callId = await this.createCall( + { + callerPhone, + direction, + callStatus, + agentName, + startTime, + endTime, + duration, + recordingUrl, + disposition, + ucid, + }, + authHeader, + ); + + this.logger.log(`Created call record: ${callId} (${callStatus})`); + + // Step 2: Find matching lead by phone number + const lead = await this.findLeadByPhone(callerPhone, authHeader); + + if (lead) { + // Step 3: Link call to lead + await this.updateCall(callId, { leadId: lead.id }, authHeader); + + // Step 4: Create lead activity + const summary = + callStatus === 'MISSED' + ? `Missed inbound call from ${callerPhone} (${duration}s, ${hangupBy ?? 'unknown'})` + : `Inbound call from ${callerPhone} — ${duration}s, ${disposition || 'no disposition'}`; + + await this.createLeadActivity( + { + leadId: lead.id, + activityType: + callStatus === 'MISSED' ? 'CALL_RECEIVED' : 'CALL_RECEIVED', + summary, + channel: 'PHONE', + performedBy: agentName ?? 'System', + durationSeconds: duration, + outcome: callStatus === 'MISSED' ? 'NO_ANSWER' : 'SUCCESSFUL', + }, + authHeader, ); - return result.createCall.id; - } - private async findLeadByPhone(phone: string, authHeader: string): Promise<{ id: string; name: string; contactAttempts: number } | null> { - const result = await this.platform.queryWithAuth( - `{ leads(first: 50) { edges { node { id name contactPhone { primaryPhoneNumber } contactAttempts lastContacted } } } }`, - undefined, - authHeader, + // Step 5: Update lead contact timestamps + await this.updateLead( + lead.id, + { + lastContacted: startTime + ? new Date(startTime).toISOString() + : new Date().toISOString(), + contactAttempts: (lead.contactAttempts ?? 0) + 1, + }, + authHeader, ); - const leads = result.leads.edges.map((e: any) => e.node); - const cleanPhone = phone.replace(/\D/g, ''); - return leads.find((l: any) => { - const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, ''); - return lp.endsWith(cleanPhone) || cleanPhone.endsWith(lp); - }) ?? null; - } - - private async updateCall(callId: string, data: Record, authHeader: string): Promise { - await this.platform.queryWithAuth( - `mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`, - { id: callId, data }, - authHeader, + this.logger.log( + `Linked call to lead ${lead.id} (${lead.name}), activity created`, ); - } - - private async createLeadActivity(data: { - leadId: string; - activityType: string; - summary: string; - channel: string; - performedBy: string; - durationSeconds: number; - outcome: string; - }, authHeader: string): Promise { - await this.platform.queryWithAuth( - `mutation($data: LeadActivityCreateInput!) { createLeadActivity(data: $data) { id } }`, - { - data: { - name: data.summary.substring(0, 80), - activityType: data.activityType, - summary: data.summary, - occurredAt: new Date().toISOString(), - performedBy: data.performedBy, - channel: data.channel, - durationSec: data.durationSeconds, - outcome: data.outcome, - leadId: data.leadId, - }, - }, - authHeader, + } else { + this.logger.log( + `No matching lead for ${callerPhone} — call record created without lead link`, ); + } + + return { + received: true, + processed: true, + callId, + leadId: lead?.id ?? null, + }; + } catch (err: any) { + const responseData = err?.response?.data + ? JSON.stringify(err.response.data) + : ''; + this.logger.error( + `Webhook processing failed: ${err.message} ${responseData}`, + ); + return { received: true, processed: false, error: String(err) }; + } + } + + private async createCall( + data: { + callerPhone: string; + direction: string; + callStatus: string; + agentName: string | null; + startTime: string | null; + endTime: string | null; + duration: number; + recordingUrl: string | null; + disposition: string | null; + ucid: string | null; + }, + authHeader: string, + ): Promise { + const callData: Record = { + name: `${data.direction === 'INBOUND' ? 'Inbound' : 'Outbound'} — ${data.callerPhone}`, + direction: data.direction, + callStatus: data.callStatus, + callerNumber: { primaryPhoneNumber: `+91${data.callerPhone}` }, + agentName: data.agentName, + startedAt: data.startTime ? new Date(data.startTime).toISOString() : null, + endedAt: data.endTime ? new Date(data.endTime).toISOString() : null, + durationSec: data.duration, + disposition: this.mapDisposition(data.disposition), + }; + if (data.recordingUrl) { + callData.recording = { + primaryLinkUrl: data.recordingUrl, + primaryLinkLabel: 'Recording', + }; } - private async updateLead(leadId: string, data: Record, authHeader: string): Promise { - await this.platform.queryWithAuth( - `mutation($id: UUID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`, - { id: leadId, data }, - authHeader, + const result = await this.platform.queryWithAuth( + `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, + { data: callData }, + authHeader, + ); + return result.createCall.id; + } + + private async findLeadByPhone( + phone: string, + authHeader: string, + ): Promise<{ id: string; name: string; contactAttempts: number } | null> { + const result = await this.platform.queryWithAuth( + `{ leads(first: 50) { edges { node { id name contactPhone { primaryPhoneNumber } contactAttempts lastContacted } } } }`, + undefined, + authHeader, + ); + const leads = result.leads.edges.map((e: any) => e.node); + const cleanPhone = phone.replace(/\D/g, ''); + + return ( + leads.find((l: any) => { + const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace( + /\D/g, + '', ); - } + return lp.endsWith(cleanPhone) || cleanPhone.endsWith(lp); + }) ?? null + ); + } - private parseDuration(timeStr: string): number { - const parts = timeStr.split(':').map(Number); - if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; - if (parts.length === 2) return parts[0] * 60 + parts[1]; - return parseInt(timeStr) || 0; - } + private async updateCall( + callId: string, + data: Record, + authHeader: string, + ): Promise { + await this.platform.queryWithAuth( + `mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`, + { id: callId, data }, + authHeader, + ); + } - private mapDisposition(disposition: string | null): string | null { - if (!disposition) return null; - const map: Record = { - 'General Enquiry': 'INFO_PROVIDED', - 'Appointment Booked': 'APPOINTMENT_BOOKED', - 'Follow Up': 'FOLLOW_UP_SCHEDULED', - 'Not Interested': 'CALLBACK_REQUESTED', - 'Wrong Number': 'WRONG_NUMBER', - }; - return map[disposition] ?? null; - } + private async createLeadActivity( + data: { + leadId: string; + activityType: string; + summary: string; + channel: string; + performedBy: string; + durationSeconds: number; + outcome: string; + }, + authHeader: string, + ): Promise { + await this.platform.queryWithAuth( + `mutation($data: LeadActivityCreateInput!) { createLeadActivity(data: $data) { id } }`, + { + data: { + name: data.summary.substring(0, 80), + activityType: data.activityType, + summary: data.summary, + occurredAt: new Date().toISOString(), + performedBy: data.performedBy, + channel: data.channel, + durationSec: data.durationSeconds, + outcome: data.outcome, + leadId: data.leadId, + }, + }, + authHeader, + ); + } + + private async updateLead( + leadId: string, + data: Record, + authHeader: string, + ): Promise { + await this.platform.queryWithAuth( + `mutation($id: UUID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`, + { id: leadId, data }, + authHeader, + ); + } + + private parseDuration(timeStr: string): number { + const parts = timeStr.split(':').map(Number); + if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + if (parts.length === 2) return parts[0] * 60 + parts[1]; + return parseInt(timeStr) || 0; + } + + private mapDisposition(disposition: string | null): string | null { + if (!disposition) return null; + const map: Record = { + 'General Enquiry': 'INFO_PROVIDED', + 'Appointment Booked': 'APPOINTMENT_BOOKED', + 'Follow Up': 'FOLLOW_UP_SCHEDULED', + 'Not Interested': 'CALLBACK_REQUESTED', + 'Wrong Number': 'WRONG_NUMBER', + }; + return map[disposition] ?? null; + } } diff --git a/src/worklist/missed-queue.service.ts b/src/worklist/missed-queue.service.ts index ee98d83..e43a35a 100644 --- a/src/worklist/missed-queue.service.ts +++ b/src/worklist/missed-queue.service.ts @@ -5,87 +5,103 @@ import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; // Normalize phone to +91XXXXXXXXXX format export function normalizePhone(raw: string): string { - let digits = raw.replace(/[^0-9]/g, ''); - // Strip leading country code variations: 0091, 91, 0 - if (digits.startsWith('0091')) digits = digits.slice(4); - else if (digits.startsWith('91') && digits.length > 10) digits = digits.slice(2); - else if (digits.startsWith('0') && digits.length > 10) digits = digits.slice(1); - return `+91${digits.slice(-10)}`; + let digits = raw.replace(/[^0-9]/g, ''); + // Strip leading country code variations: 0091, 91, 0 + if (digits.startsWith('0091')) digits = digits.slice(4); + else if (digits.startsWith('91') && digits.length > 10) + digits = digits.slice(2); + else if (digits.startsWith('0') && digits.length > 10) + digits = digits.slice(1); + return `+91${digits.slice(-10)}`; } @Injectable() export class MissedQueueService implements OnModuleInit { - private readonly logger = new Logger(MissedQueueService.name); - private readonly pollIntervalMs: number; - private readonly processedUcids = new Set(); - private assignmentMutex = false; + private readonly logger = new Logger(MissedQueueService.name); + private readonly pollIntervalMs: number; + private readonly processedUcids = new Set(); + private assignmentMutex = false; - constructor( - private readonly config: ConfigService, - private readonly platform: PlatformGraphqlService, - private readonly ozonetel: OzonetelAgentService, - ) { - this.pollIntervalMs = this.config.get('missedQueue.pollIntervalMs', 30000); + constructor( + private readonly config: ConfigService, + private readonly platform: PlatformGraphqlService, + private readonly ozonetel: OzonetelAgentService, + ) { + this.pollIntervalMs = this.config.get( + 'missedQueue.pollIntervalMs', + 30000, + ); + } + + onModuleInit() { + this.logger.log( + `Starting missed call ingestion polling every ${this.pollIntervalMs}ms`, + ); + setInterval( + () => + this.ingest().catch((err) => + this.logger.error('Ingestion failed', err), + ), + this.pollIntervalMs, + ); + } + + async ingest(): Promise<{ created: number; updated: number }> { + let created = 0; + let updated = 0; + + const now = new Date(); + const fiveMinAgo = new Date(now.getTime() - 5 * 60 * 1000); + const format = (d: Date) => d.toISOString().replace('T', ' ').slice(0, 19); + + let abandonCalls: any[]; + try { + abandonCalls = await this.ozonetel.getAbandonCalls({ + fromTime: format(fiveMinAgo), + toTime: format(now), + }); + } catch (err) { + this.logger.warn(`Failed to fetch abandon calls: ${err}`); + return { created: 0, updated: 0 }; } - onModuleInit() { - this.logger.log(`Starting missed call ingestion polling every ${this.pollIntervalMs}ms`); - setInterval(() => this.ingest().catch(err => this.logger.error('Ingestion failed', err)), this.pollIntervalMs); - } + if (!abandonCalls?.length) return { created: 0, updated: 0 }; - async ingest(): Promise<{ created: number; updated: number }> { - let created = 0; - let updated = 0; + for (const call of abandonCalls) { + const ucid = call.monitorUCID; + if (!ucid || this.processedUcids.has(ucid)) continue; + this.processedUcids.add(ucid); - const now = new Date(); - const fiveMinAgo = new Date(now.getTime() - 5 * 60 * 1000); - const format = (d: Date) => d.toISOString().replace('T', ' ').slice(0, 19); + const phone = normalizePhone(call.callerID || ''); + if (!phone || phone.length < 13) continue; - let abandonCalls: any[]; - try { - abandonCalls = await this.ozonetel.getAbandonCalls({ fromTime: format(fiveMinAgo), toTime: format(now) }); - } catch (err) { - this.logger.warn(`Failed to fetch abandon calls: ${err}`); - return { created: 0, updated: 0 }; - } + const did = call.did || ''; + const callTime = call.callTime || new Date().toISOString(); - if (!abandonCalls?.length) return { created: 0, updated: 0 }; - - for (const call of abandonCalls) { - const ucid = call.monitorUCID; - if (!ucid || this.processedUcids.has(ucid)) continue; - this.processedUcids.add(ucid); - - const phone = normalizePhone(call.callerID || ''); - if (!phone || phone.length < 13) continue; - - const did = call.did || ''; - const callTime = call.callTime || new Date().toISOString(); - - try { - const existing = await this.platform.query( - `{ calls(first: 1, filter: { + try { + const existing = await this.platform.query( + `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, callerNumber: { primaryPhoneNumber: { eq: "${phone}" } } }) { edges { node { id missedcallcount } } } }`, - ); + ); - const existingNode = existing?.calls?.edges?.[0]?.node; + const existingNode = existing?.calls?.edges?.[0]?.node; - if (existingNode) { - const newCount = (existingNode.missedcallcount || 1) + 1; - await this.platform.query( - `mutation { updateCall(id: "${existingNode.id}", data: { + if (existingNode) { + const newCount = (existingNode.missedcallcount || 1) + 1; + await this.platform.query( + `mutation { updateCall(id: "${existingNode.id}", data: { missedcallcount: ${newCount}, startedAt: "${callTime}", callsourcenumber: "${did}" }) { id } }`, - ); - updated++; - this.logger.log(`Dedup missed call ${phone}: count now ${newCount}`); - } else { - await this.platform.query( - `mutation { createCall(data: { + ); + updated++; + this.logger.log(`Dedup missed call ${phone}: count now ${newCount}`); + } else { + await this.platform.query( + `mutation { createCall(data: { callStatus: MISSED, direction: INBOUND, callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }, @@ -94,34 +110,35 @@ export class MissedQueueService implements OnModuleInit { missedcallcount: 1, startedAt: "${callTime}" }) { id } }`, - ); - created++; - this.logger.log(`Created missed call record for ${phone}`); - } - } catch (err) { - this.logger.warn(`Failed to process abandon call ${ucid}: ${err}`); - } + ); + created++; + this.logger.log(`Created missed call record for ${phone}`); } - - // Trim processedUcids to prevent unbounded growth - if (this.processedUcids.size > 500) { - const arr = Array.from(this.processedUcids); - this.processedUcids.clear(); - arr.slice(-200).forEach(u => this.processedUcids.add(u)); - } - - if (created || updated) this.logger.log(`Ingestion: ${created} created, ${updated} updated`); - return { created, updated }; + } catch (err) { + this.logger.warn(`Failed to process abandon call ${ucid}: ${err}`); + } } - async assignNext(agentName: string): Promise { - if (this.assignmentMutex) return null; - this.assignmentMutex = true; + // Trim processedUcids to prevent unbounded growth + if (this.processedUcids.size > 500) { + const arr = Array.from(this.processedUcids); + this.processedUcids.clear(); + arr.slice(-200).forEach((u) => this.processedUcids.add(u)); + } - try { - // Find oldest unassigned PENDING_CALLBACK call (empty agentName) - let result = await this.platform.query( - `{ calls(first: 1, filter: { + if (created || updated) + this.logger.log(`Ingestion: ${created} created, ${updated} updated`); + return { created, updated }; + } + + async assignNext(agentName: string): Promise { + if (this.assignmentMutex) return null; + this.assignmentMutex = true; + + try { + // Find oldest unassigned PENDING_CALLBACK call (empty agentName) + let result = await this.platform.query( + `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, agentName: { eq: "" } }, orderBy: [{ startedAt: AscNullsLast }]) { @@ -130,14 +147,14 @@ export class MissedQueueService implements OnModuleInit { startedAt callsourcenumber missedcallcount } } } }`, - ); + ); - let call = result?.calls?.edges?.[0]?.node; + let call = result?.calls?.edges?.[0]?.node; - // Also check for null agentName - if (!call) { - result = await this.platform.query( - `{ calls(first: 1, filter: { + // Also check for null agentName + if (!call) { + result = await this.platform.query( + `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, agentName: { is: NULL } }, orderBy: [{ startedAt: AscNullsLast }]) { @@ -146,80 +163,117 @@ export class MissedQueueService implements OnModuleInit { startedAt callsourcenumber missedcallcount } } } }`, - ); - call = result?.calls?.edges?.[0]?.node; - } - - if (!call) return null; - - await this.platform.query( - `mutation { updateCall(id: "${call.id}", data: { agentName: "${agentName}" }) { id } }`, - ); - this.logger.log(`Assigned missed call ${call.id} to ${agentName}`); - return call; - } catch (err) { - this.logger.warn(`Assignment failed: ${err}`); - return null; - } finally { - this.assignmentMutex = false; - } - } - - async updateStatus(callId: string, status: string, authHeader: string): Promise { - const validStatuses = ['PENDING_CALLBACK', 'CALLBACK_ATTEMPTED', 'CALLBACK_COMPLETED', 'INVALID', 'WRONG_NUMBER']; - if (!validStatuses.includes(status)) { - throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`); - } - - const dataParts: string[] = [`callbackstatus: ${status}`]; - if (status === 'CALLBACK_ATTEMPTED') { - dataParts.push(`callbackattemptedat: "${new Date().toISOString()}"`); - } - - return this.platform.queryWithAuth( - `mutation { updateCall(id: "${callId}", data: { ${dataParts.join(', ')} }) { id callbackstatus callbackattemptedat } }`, - undefined, - authHeader, ); + call = result?.calls?.edges?.[0]?.node; + } + + if (!call) return null; + + await this.platform.query( + `mutation { updateCall(id: "${call.id}", data: { agentName: "${agentName}" }) { id } }`, + ); + this.logger.log(`Assigned missed call ${call.id} to ${agentName}`); + return call; + } catch (err) { + this.logger.warn(`Assignment failed: ${err}`); + return null; + } finally { + this.assignmentMutex = false; + } + } + + async updateStatus( + callId: string, + status: string, + authHeader: string, + ): Promise { + const validStatuses = [ + 'PENDING_CALLBACK', + 'CALLBACK_ATTEMPTED', + 'CALLBACK_COMPLETED', + 'INVALID', + 'WRONG_NUMBER', + ]; + if (!validStatuses.includes(status)) { + throw new Error( + `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`, + ); } - async getMissedQueue(agentName: string, authHeader: string): Promise<{ - pending: any[]; - attempted: any[]; - completed: any[]; - invalid: any[]; - }> { - const fields = `id name createdAt direction callStatus agentName + const dataParts: string[] = [`callbackstatus: ${status}`]; + if (status === 'CALLBACK_ATTEMPTED') { + dataParts.push(`callbackattemptedat: "${new Date().toISOString()}"`); + } + + return this.platform.queryWithAuth( + `mutation { updateCall(id: "${callId}", data: { ${dataParts.join(', ')} }) { id callbackstatus callbackattemptedat } }`, + undefined, + authHeader, + ); + } + + async getMissedQueue( + agentName: string, + authHeader: string, + ): Promise<{ + pending: any[]; + attempted: any[]; + completed: any[]; + invalid: any[]; + }> { + const fields = `id name createdAt direction callStatus agentName callerNumber { primaryPhoneNumber } startedAt endedAt durationSec disposition leadId callbackstatus callsourcenumber missedcallcount callbackattemptedat`; - const buildQuery = (status: string) => `{ calls(first: 50, filter: { + const buildQuery = (status: string) => `{ calls(first: 50, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: MISSED }, callbackstatus: { eq: ${status} } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { ${fields} } } } }`; - try { - const [pending, attempted, completed, invalid, wrongNumber] = await Promise.all([ - this.platform.queryWithAuth(buildQuery('PENDING_CALLBACK'), undefined, authHeader), - this.platform.queryWithAuth(buildQuery('CALLBACK_ATTEMPTED'), undefined, authHeader), - this.platform.queryWithAuth(buildQuery('CALLBACK_COMPLETED'), undefined, authHeader), - this.platform.queryWithAuth(buildQuery('INVALID'), undefined, authHeader), - this.platform.queryWithAuth(buildQuery('WRONG_NUMBER'), undefined, authHeader), - ]); + try { + const [pending, attempted, completed, invalid, wrongNumber] = + await Promise.all([ + this.platform.queryWithAuth( + buildQuery('PENDING_CALLBACK'), + undefined, + authHeader, + ), + this.platform.queryWithAuth( + buildQuery('CALLBACK_ATTEMPTED'), + undefined, + authHeader, + ), + this.platform.queryWithAuth( + buildQuery('CALLBACK_COMPLETED'), + undefined, + authHeader, + ), + this.platform.queryWithAuth( + buildQuery('INVALID'), + undefined, + authHeader, + ), + this.platform.queryWithAuth( + buildQuery('WRONG_NUMBER'), + undefined, + authHeader, + ), + ]); - const extract = (r: any) => r?.calls?.edges?.map((e: any) => e.node) ?? []; + const extract = (r: any) => + r?.calls?.edges?.map((e: any) => e.node) ?? []; - return { - pending: extract(pending), - attempted: extract(attempted), - completed: [...extract(completed), ...extract(wrongNumber)], - invalid: extract(invalid), - }; - } catch (err) { - this.logger.warn(`Failed to fetch missed queue: ${err}`); - return { pending: [], attempted: [], completed: [], invalid: [] }; - } + return { + pending: extract(pending), + attempted: extract(attempted), + completed: [...extract(completed), ...extract(wrongNumber)], + invalid: extract(invalid), + }; + } catch (err) { + this.logger.warn(`Failed to fetch missed queue: ${err}`); + return { pending: [], attempted: [], completed: [], invalid: [] }; } + } } diff --git a/src/worklist/worklist.controller.ts b/src/worklist/worklist.controller.ts index de3b9d5..c9e3aa9 100644 --- a/src/worklist/worklist.controller.ts +++ b/src/worklist/worklist.controller.ts @@ -1,61 +1,72 @@ -import { Controller, Get, Patch, Headers, Param, Body, HttpException, Logger } from '@nestjs/common'; +import { + Controller, + Get, + Patch, + Headers, + Param, + Body, + HttpException, + Logger, +} from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { WorklistService } from './worklist.service'; import { MissedQueueService } from './missed-queue.service'; @Controller('api/worklist') export class WorklistController { - private readonly logger = new Logger(WorklistController.name); + private readonly logger = new Logger(WorklistController.name); - constructor( - private readonly worklist: WorklistService, - private readonly missedQueue: MissedQueueService, - private readonly platform: PlatformGraphqlService, - ) {} + constructor( + private readonly worklist: WorklistService, + private readonly missedQueue: MissedQueueService, + private readonly platform: PlatformGraphqlService, + ) {} - @Get() - async getWorklist(@Headers('authorization') authHeader: string) { - if (!authHeader) { - throw new HttpException('Authorization required', 401); - } - - const agentName = await this.resolveAgentName(authHeader); - this.logger.log(`Fetching worklist for agent: ${agentName}`); - - return this.worklist.getWorklist(agentName, authHeader); + @Get() + async getWorklist(@Headers('authorization') authHeader: string) { + if (!authHeader) { + throw new HttpException('Authorization required', 401); } - @Get('missed-queue') - async getMissedQueue(@Headers('authorization') authHeader: string) { - if (!authHeader) throw new HttpException('Authorization header required', 401); - const agentName = await this.resolveAgentName(authHeader); - return this.missedQueue.getMissedQueue(agentName, authHeader); - } + const agentName = await this.resolveAgentName(authHeader); + this.logger.log(`Fetching worklist for agent: ${agentName}`); - @Patch('missed-queue/:id/status') - async updateMissedCallStatus( - @Param('id') id: string, - @Headers('authorization') authHeader: string, - @Body() body: { status: string }, - ) { - if (!authHeader) throw new HttpException('Authorization header required', 401); - if (!body.status) throw new HttpException('status is required', 400); - return this.missedQueue.updateStatus(id, body.status, authHeader); - } + return this.worklist.getWorklist(agentName, authHeader); + } - private async resolveAgentName(authHeader: string): Promise { - try { - const data = await this.platform.queryWithAuth( - `{ currentUser { workspaceMember { name { firstName lastName } } } }`, - undefined, - authHeader, - ); - const name = data.currentUser?.workspaceMember?.name; - const full = `${name?.firstName ?? ''} ${name?.lastName ?? ''}`.trim(); - if (full) return full; - } catch (err) { - this.logger.warn(`Failed to resolve agent name: ${err}`); - } - throw new HttpException('Could not determine agent identity', 400); + @Get('missed-queue') + async getMissedQueue(@Headers('authorization') authHeader: string) { + if (!authHeader) + throw new HttpException('Authorization header required', 401); + const agentName = await this.resolveAgentName(authHeader); + return this.missedQueue.getMissedQueue(agentName, authHeader); + } + + @Patch('missed-queue/:id/status') + async updateMissedCallStatus( + @Param('id') id: string, + @Headers('authorization') authHeader: string, + @Body() body: { status: string }, + ) { + if (!authHeader) + throw new HttpException('Authorization header required', 401); + if (!body.status) throw new HttpException('status is required', 400); + return this.missedQueue.updateStatus(id, body.status, authHeader); + } + + private async resolveAgentName(authHeader: string): Promise { + try { + const data = await this.platform.queryWithAuth( + `{ currentUser { workspaceMember { name { firstName lastName } } } }`, + undefined, + authHeader, + ); + const name = data.currentUser?.workspaceMember?.name; + const full = `${name?.firstName ?? ''} ${name?.lastName ?? ''}`.trim(); + if (full) return full; + } catch (err) { + this.logger.warn(`Failed to resolve agent name: ${err}`); } + throw new HttpException('Could not determine agent identity', 400); + } } diff --git a/src/worklist/worklist.module.ts b/src/worklist/worklist.module.ts index cfc64c6..155d5ca 100644 --- a/src/worklist/worklist.module.ts +++ b/src/worklist/worklist.module.ts @@ -8,9 +8,13 @@ import { MissedCallWebhookController } from './missed-call-webhook.controller'; import { KookooCallbackController } from './kookoo-callback.controller'; @Module({ - imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)], - controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController], - providers: [WorklistService, MissedQueueService], - exports: [MissedQueueService], + imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)], + controllers: [ + WorklistController, + MissedCallWebhookController, + KookooCallbackController, + ], + providers: [WorklistService, MissedQueueService], + exports: [MissedQueueService], }) export class WorklistModule {} diff --git a/src/worklist/worklist.service.ts b/src/worklist/worklist.service.ts index 81bc8bb..c32c540 100644 --- a/src/worklist/worklist.service.ts +++ b/src/worklist/worklist.service.ts @@ -2,37 +2,44 @@ import { Injectable, Logger } from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; export type WorklistResponse = { - missedCalls: any[]; - followUps: any[]; - marketingLeads: any[]; - totalPending: number; + missedCalls: any[]; + followUps: any[]; + marketingLeads: any[]; + totalPending: number; }; @Injectable() export class WorklistService { - private readonly logger = new Logger(WorklistService.name); + private readonly logger = new Logger(WorklistService.name); - constructor(private readonly platform: PlatformGraphqlService) {} + constructor(private readonly platform: PlatformGraphqlService) {} - async getWorklist(agentName: string, authHeader: string): Promise { - const [missedCalls, followUps, marketingLeads] = await Promise.all([ - this.getMissedCalls(agentName, authHeader), - this.getPendingFollowUps(agentName, authHeader), - this.getAssignedLeads(agentName, authHeader), - ]); + async getWorklist( + agentName: string, + authHeader: string, + ): Promise { + const [missedCalls, followUps, marketingLeads] = await Promise.all([ + this.getMissedCalls(agentName, authHeader), + this.getPendingFollowUps(agentName, authHeader), + this.getAssignedLeads(agentName, authHeader), + ]); - return { - missedCalls, - followUps, - marketingLeads, - totalPending: missedCalls.length + followUps.length + marketingLeads.length, - }; - } + return { + missedCalls, + followUps, + marketingLeads, + totalPending: + missedCalls.length + followUps.length + marketingLeads.length, + }; + } - private async getAssignedLeads(agentName: string, authHeader: string): Promise { - try { - const data = await this.platform.queryWithAuth( - `{ leads(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }, orderBy: [{ createdAt: AscNullsLast }]) { edges { node { + private async getAssignedLeads( + agentName: string, + authHeader: string, + ): Promise { + try { + const data = await this.platform.queryWithAuth( + `{ leads(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }, orderBy: [{ createdAt: AscNullsLast }]) { edges { node { id createdAt contactName { firstName lastName } contactPhone { primaryPhoneNumber } @@ -42,43 +49,49 @@ export class WorklistService { contactAttempts spamScore isSpam aiSummary aiSuggestedAction } } } }`, - undefined, - authHeader, - ); - return data.leads.edges.map((e: any) => e.node); - } catch (err) { - this.logger.warn(`Failed to fetch assigned leads: ${err}`); - return []; - } + undefined, + authHeader, + ); + return data.leads.edges.map((e: any) => e.node); + } catch (err) { + this.logger.warn(`Failed to fetch assigned leads: ${err}`); + return []; } + } - private async getPendingFollowUps(agentName: string, authHeader: string): Promise { - try { - const data = await this.platform.queryWithAuth( - `{ followUps(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }) { edges { node { + private async getPendingFollowUps( + agentName: string, + authHeader: string, + ): Promise { + try { + const data = await this.platform.queryWithAuth( + `{ followUps(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }) { edges { node { id name createdAt typeCustom status scheduledAt completedAt priority assignedAgent patientId } } } }`, - undefined, - authHeader, - ); - // Filter to PENDING/OVERDUE client-side since platform may not support in-filter on remapped fields - return data.followUps.edges - .map((e: any) => e.node) - .filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE'); - } catch (err) { - this.logger.warn(`Failed to fetch follow-ups: ${err}`); - return []; - } + undefined, + authHeader, + ); + // Filter to PENDING/OVERDUE client-side since platform may not support in-filter on remapped fields + return data.followUps.edges + .map((e: any) => e.node) + .filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE'); + } catch (err) { + this.logger.warn(`Failed to fetch follow-ups: ${err}`); + return []; } + } - private async getMissedCalls(agentName: string, authHeader: string): Promise { - try { - // FIFO ordering (AscNullsLast) — oldest first. Filter to active callback statuses only. - const data = await this.platform.queryWithAuth( - `{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: MISSED }, callbackstatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { + private async getMissedCalls( + agentName: string, + authHeader: string, + ): Promise { + try { + // FIFO ordering (AscNullsLast) — oldest first. Filter to active callback statuses only. + const data = await this.platform.queryWithAuth( + `{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: MISSED }, callbackstatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { id name createdAt direction callStatus agentName callerNumber { primaryPhoneNumber } @@ -86,13 +99,13 @@ export class WorklistService { disposition leadId callbackstatus callsourcenumber missedcallcount callbackattemptedat } } } }`, - undefined, - authHeader, - ); - return data.calls.edges.map((e: any) => e.node); - } catch (err) { - this.logger.warn(`Failed to fetch missed calls: ${err}`); - return []; - } + undefined, + authHeader, + ); + return data.calls.edges.map((e: any) => e.node); + } catch (err) { + this.logger.warn(`Failed to fetch missed calls: ${err}`); + return []; } + } }