diff --git a/omni_chat/Dockerfile b/backend/Dockerfile similarity index 100% rename from omni_chat/Dockerfile rename to backend/Dockerfile diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..fadd45a --- /dev/null +++ b/backend/README.md @@ -0,0 +1,35 @@ +# backend + +Core CRM/omni-домен с единственной Prisma-базой. + +## Назначение + +- принимает входящие telegram-события через GraphQL mutation `ingestTelegramInbound`; +- создает исходящую задачу через GraphQL mutation `requestTelegramOutbound` (в `telegram_backend`, далее в Hatchet); +- принимает отчет о доставке через GraphQL mutation `reportTelegramOutbound`. + +## API + +- `GET /health` +- `POST /graphql` + +## GraphQL auth + +Если задан `BACKEND_GRAPHQL_SHARED_SECRET`, запросы на `/graphql` должны содержать заголовок: + +- `x-graphql-secret: ` + +## Переменные окружения + +- `PORT` (default: `8090`) +- `MAX_BODY_SIZE_BYTES` (default: `2097152`) +- `BACKEND_GRAPHQL_SHARED_SECRET` (optional) +- `TELEGRAM_BACKEND_GRAPHQL_URL` (required для `requestTelegramOutbound`) +- `TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET` (optional) +- `DEFAULT_TEAM_ID` (optional fallback для inbound маршрутизации) + +## Prisma policy + +- Источник схемы: `Frontend/prisma/schema.prisma`. +- Локальная копия в `backend/prisma/schema.prisma` обновляется только через `scripts/prisma-sync.sh`. +- Миграции/`db push` выполняются только в `Frontend`. diff --git a/omni_chat/package-lock.json b/backend/package-lock.json similarity index 72% rename from omni_chat/package-lock.json rename to backend/package-lock.json index 197238c..e453069 100644 --- a/omni_chat/package-lock.json +++ b/backend/package-lock.json @@ -1,14 +1,13 @@ { - "name": "crm-omni-chat", + "name": "crm-backend", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "crm-omni-chat", - "hasInstallScript": true, + "name": "crm-backend", "dependencies": { "@prisma/client": "^6.16.1", - "bullmq": "^5.70.0" + "graphql": "^16.13.1" }, "devDependencies": { "@types/node": "^22.13.9", @@ -459,90 +458,6 @@ "node": ">=18" } }, - "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", - "license": "MIT" - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@prisma/client": { "version": "6.16.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz", @@ -645,21 +560,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/bullmq": { - "version": "5.70.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.70.0.tgz", - "integrity": "sha512-HlBSEJqG7MJ97+d/N/8rtGOcpisjGP3WD/zaXZia0hsmckJqAPTVWN6Yfw32FVfVSUVVInZQ2nUgMd2zCRghKg==", - "license": "MIT", - "dependencies": { - "cron-parser": "4.9.0", - "ioredis": "5.9.2", - "msgpackr": "1.11.5", - "node-abort-controller": "3.1.1", - "semver": "7.7.4", - "tslib": "2.8.1", - "uuid": "11.1.0" - } - }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -715,15 +615,6 @@ "consola": "^3.2.3" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -741,35 +632,6 @@ "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -787,15 +649,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -803,16 +656,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -965,28 +808,13 @@ "giget": "dist/cli.mjs" } }, - "node_modules/ioredis": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", - "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", + "node_modules/graphql": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.5.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/jiti": { @@ -999,70 +827,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -1070,21 +834,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/nypm": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", @@ -1211,27 +960,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1242,24 +970,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -1270,12 +980,6 @@ "node": ">=18" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1316,19 +1020,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } } } } diff --git a/omni_outbound/package.json b/backend/package.json similarity index 62% rename from omni_outbound/package.json rename to backend/package.json index 2d7b5db..1d457b6 100644 --- a/omni_outbound/package.json +++ b/backend/package.json @@ -1,20 +1,19 @@ { - "name": "crm-omni-outbound", + "name": "crm-backend", "private": true, "type": "module", "scripts": { - "db:generate": "prisma generate", - "start": "tsx src/worker.ts" - }, - "dependencies": { - "@prisma/client": "^6.16.1", - "bullmq": "^5.58.2", - "ioredis": "^5.7.0" + "start": "tsx src/index.ts", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@types/node": "^22.13.9", "prisma": "^6.16.1", "tsx": "^4.20.5", "typescript": "^5.9.2" + }, + "dependencies": { + "@prisma/client": "^6.16.1", + "graphql": "^16.13.1" } } diff --git a/omni_chat/prisma/schema.prisma b/backend/prisma/schema.prisma similarity index 88% rename from omni_chat/prisma/schema.prisma rename to backend/prisma/schema.prisma index cdb7221..19204e8 100644 --- a/omni_chat/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,10 +1,10 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client" + output = "../server/generated/prisma" } datasource db { provider = "postgresql" - url = env("DATABASE_URL") } enum TeamRole { @@ -58,6 +58,12 @@ enum WorkspaceDocumentType { Template } +enum ClientTimelineContentType { + CALENDAR_EVENT + DOCUMENT + RECOMMENDATION +} + model Team { id String @id @default(cuid()) name String @@ -79,8 +85,10 @@ model Team { feedCards FeedCard[] contactPins ContactPin[] documents WorkspaceDocument[] + clientTimelineEntries ClientTimelineEntry[] contactInboxes ContactInbox[] contactInboxPreferences ContactInboxPreference[] + contactThreadReads ContactThreadRead[] } model User { @@ -96,6 +104,7 @@ model User { aiConversations AiConversation[] @relation("ConversationCreator") aiMessages AiMessage[] @relation("ChatAuthor") contactInboxPreferences ContactInboxPreference[] + contactThreadReads ContactThreadRead[] } model TeamMember { @@ -134,10 +143,29 @@ model Contact { omniMessages OmniMessage[] omniIdentities OmniContactIdentity[] contactInboxes ContactInbox[] + clientTimelineEntries ClientTimelineEntry[] + contactThreadReads ContactThreadRead[] @@index([teamId, updatedAt]) } +model ContactThreadRead { + id String @id @default(cuid()) + teamId String + userId String + contactId String + readAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + + @@unique([userId, contactId]) + @@index([teamId, userId]) +} + model ContactNote { id String @id @default(cuid()) contactId String @unique @@ -435,3 +463,21 @@ model WorkspaceDocument { @@index([teamId, updatedAt]) } + +model ClientTimelineEntry { + id String @id @default(cuid()) + teamId String + contactId String + contentType ClientTimelineContentType + contentId String + datetime DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + + @@unique([teamId, contentType, contentId]) + @@index([teamId, contactId, datetime]) + @@index([contactId, datetime]) +} diff --git a/omni_inbound/src/index.ts b/backend/src/index.ts similarity index 80% rename from omni_inbound/src/index.ts rename to backend/src/index.ts index 1242091..2b0702b 100644 --- a/omni_inbound/src/index.ts +++ b/backend/src/index.ts @@ -1,10 +1,10 @@ -import { closeInboundQueue } from "./queue"; import { startServer } from "./server"; +import { prisma } from "./utils/prisma"; const server = startServer(); async function shutdown(signal: string) { - console.log(`[omni_inbound] shutting down by ${signal}`); + console.log(`[backend] shutting down by ${signal}`); try { await new Promise((resolve, reject) => { @@ -21,7 +21,7 @@ async function shutdown(signal: string) { } try { - await closeInboundQueue(); + await prisma.$disconnect(); } catch { // ignore shutdown errors } diff --git a/backend/src/server.ts b/backend/src/server.ts new file mode 100644 index 0000000..c377722 --- /dev/null +++ b/backend/src/server.ts @@ -0,0 +1,232 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { buildSchema, graphql } from "graphql"; +import { + ingestTelegramInbound, + reportTelegramOutbound, + requestTelegramOutbound, + type TelegramInboundEnvelope, + type TelegramOutboundReport, + type TelegramOutboundRequest, +} from "./service"; + +const PORT = Number(process.env.PORT || 8090); +const MAX_BODY_SIZE_BYTES = Number(process.env.MAX_BODY_SIZE_BYTES || 2 * 1024 * 1024); +const GRAPHQL_SHARED_SECRET = String(process.env.BACKEND_GRAPHQL_SHARED_SECRET || "").trim(); + +const schema = buildSchema(` + type Query { + health: Health! + } + + type Health { + ok: Boolean! + service: String! + now: String! + } + + type MutationResult { + ok: Boolean! + message: String! + runId: String + omniMessageId: String + } + + input TelegramInboundInput { + version: Int! + idempotencyKey: String! + provider: String! + channel: String! + direction: String! + providerEventId: String! + providerMessageId: String + eventType: String! + occurredAt: String! + receivedAt: String! + payloadRawJson: String! + payloadNormalizedJson: String! + } + + input TelegramOutboundReportInput { + omniMessageId: String! + status: String! + providerMessageId: String + error: String + responseJson: String + } + + input TelegramOutboundTaskInput { + omniMessageId: String! + chatId: String! + text: String! + businessConnectionId: String + } + + type Mutation { + ingestTelegramInbound(input: TelegramInboundInput!): MutationResult! + reportTelegramOutbound(input: TelegramOutboundReportInput!): MutationResult! + requestTelegramOutbound(input: TelegramOutboundTaskInput!): MutationResult! + } +`); + +function writeJson(res: ServerResponse, statusCode: number, body: unknown) { + res.statusCode = statusCode; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify(body)); +} + +function isGraphqlAuthorized(req: IncomingMessage) { + if (!GRAPHQL_SHARED_SECRET) return true; + const incoming = String(req.headers["x-graphql-secret"] || "").trim(); + return incoming !== "" && incoming === GRAPHQL_SHARED_SECRET; +} + +async function readJsonBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + let total = 0; + + for await (const chunk of req) { + const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + total += buf.length; + if (total > MAX_BODY_SIZE_BYTES) { + throw new Error(`payload_too_large:${MAX_BODY_SIZE_BYTES}`); + } + chunks.push(buf); + } + + const raw = Buffer.concat(chunks).toString("utf8"); + if (!raw) return {}; + return JSON.parse(raw); +} + +function parseJsonField(raw: string, fieldName: string): T { + try { + return JSON.parse(raw) as T; + } catch { + throw new Error(`${fieldName} must be valid JSON string`); + } +} + +const root = { + health: () => ({ + ok: true, + service: "backend", + now: new Date().toISOString(), + }), + + ingestTelegramInbound: async ({ input }: { input: any }) => { + const envelope: TelegramInboundEnvelope = { + version: Number(input.version ?? 1), + idempotencyKey: String(input.idempotencyKey ?? ""), + provider: String(input.provider ?? ""), + channel: String(input.channel ?? ""), + direction: String(input.direction ?? "IN") === "OUT" ? "OUT" : "IN", + providerEventId: String(input.providerEventId ?? ""), + providerMessageId: input.providerMessageId != null ? String(input.providerMessageId) : null, + eventType: String(input.eventType ?? ""), + occurredAt: String(input.occurredAt ?? new Date().toISOString()), + receivedAt: String(input.receivedAt ?? new Date().toISOString()), + payloadRaw: parseJsonField(input.payloadRawJson, "payloadRawJson"), + payloadNormalized: parseJsonField(input.payloadNormalizedJson, "payloadNormalizedJson"), + }; + + const result = await ingestTelegramInbound(envelope); + return { + ok: result.ok, + message: result.message, + omniMessageId: (result as any).omniMessageId ?? null, + runId: null, + }; + }, + + reportTelegramOutbound: async ({ input }: { input: any }) => { + const payload: TelegramOutboundReport = { + omniMessageId: String(input.omniMessageId ?? ""), + status: String(input.status ?? "FAILED"), + providerMessageId: input.providerMessageId != null ? String(input.providerMessageId) : null, + error: input.error != null ? String(input.error) : null, + responseJson: input.responseJson != null ? String(input.responseJson) : null, + }; + + const result = await reportTelegramOutbound(payload); + return { + ok: result.ok, + message: result.message, + runId: null, + omniMessageId: null, + }; + }, + + requestTelegramOutbound: async ({ input }: { input: any }) => { + const payload: TelegramOutboundRequest = { + omniMessageId: String(input.omniMessageId ?? ""), + chatId: String(input.chatId ?? ""), + text: String(input.text ?? ""), + businessConnectionId: input.businessConnectionId != null ? String(input.businessConnectionId) : null, + }; + + const result = await requestTelegramOutbound(payload); + return { + ok: result.ok, + message: result.message, + runId: result.runId ?? null, + omniMessageId: null, + }; + }, +}; + +export function startServer() { + const server = createServer(async (req, res) => { + if (!req.url || !req.method) { + writeJson(res, 404, { ok: false, error: "not_found" }); + return; + } + + if (req.url === "/health" && req.method === "GET") { + writeJson(res, 200, { + ok: true, + service: "backend", + now: new Date().toISOString(), + }); + return; + } + + if (req.url === "/graphql" && req.method === "POST") { + if (!isGraphqlAuthorized(req)) { + writeJson(res, 401, { errors: [{ message: "unauthorized" }] }); + return; + } + + try { + const body = (await readJsonBody(req)) as { + query?: string; + variables?: Record; + operationName?: string; + }; + + const result = await graphql({ + schema, + source: String(body.query || ""), + rootValue: root, + variableValues: body.variables || {}, + operationName: body.operationName, + }); + + writeJson(res, 200, result); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const statusCode = message.startsWith("payload_too_large:") ? 413 : 400; + writeJson(res, statusCode, { errors: [{ message }] }); + } + + return; + } + + writeJson(res, 404, { ok: false, error: "not_found" }); + }); + + server.listen(PORT, "0.0.0.0", () => { + console.log(`[backend] listening on :${PORT}`); + }); + + return server; +} diff --git a/backend/src/service.ts b/backend/src/service.ts new file mode 100644 index 0000000..e407966 --- /dev/null +++ b/backend/src/service.ts @@ -0,0 +1,512 @@ +type MessageDirection = "IN" | "OUT"; +type OmniMessageStatus = "PENDING" | "SENT" | "FAILED" | "DELIVERED" | "READ"; +import { prisma } from "./utils/prisma"; + +export type TelegramInboundEnvelope = { + version: number; + idempotencyKey: string; + provider: string; + channel: string; + direction: "IN" | "OUT"; + providerEventId: string; + providerMessageId: string | null; + eventType: string; + occurredAt: string; + receivedAt: string; + payloadRaw: unknown; + payloadNormalized: { + threadExternalId: string | null; + contactExternalId: string | null; + text: string | null; + businessConnectionId: string | null; + [key: string]: unknown; + }; +}; + +export type TelegramOutboundReport = { + omniMessageId: string; + status: string; + providerMessageId?: string | null; + error?: string | null; + responseJson?: string | null; +}; + +export type TelegramOutboundRequest = { + omniMessageId: string; + chatId: string; + text: string; + businessConnectionId?: string | null; +}; + +function asString(value: unknown) { + if (typeof value !== "string") return null; + const v = value.trim(); + return v || null; +} + +function parseDate(value: string) { + const d = new Date(value); + if (Number.isNaN(d.getTime())) return new Date(); + return d; +} + +function normalizeDirection(value: string): MessageDirection { + return value === "OUT" ? "OUT" : "IN"; +} + +async function resolveTeamId(envelope: TelegramInboundEnvelope) { + const n = envelope.payloadNormalized; + const bcId = asString(n.businessConnectionId); + + if (bcId) { + const linked = await prisma.telegramBusinessConnection.findFirst({ + where: { businessConnectionId: bcId }, + orderBy: { updatedAt: "desc" }, + select: { teamId: true }, + }); + if (linked?.teamId) return linked.teamId; + } + + const externalContactId = asString(n.contactExternalId) ?? asString(n.threadExternalId); + if (externalContactId) { + const linked = await prisma.telegramBusinessConnection.findFirst({ + where: { businessConnectionId: `link:${externalContactId}` }, + orderBy: { updatedAt: "desc" }, + select: { teamId: true }, + }); + if (linked?.teamId) return linked.teamId; + } + + const fallback = asString(process.env.DEFAULT_TEAM_ID); + if (fallback) return fallback; + + const firstTeam = await prisma.team.findFirst({ + orderBy: { createdAt: "asc" }, + select: { id: true }, + }); + + return firstTeam?.id ?? null; +} + +async function resolveContact(input: { + teamId: string; + externalContactId: string; + displayName: string; + avatarUrl: string | null; +}) { + const existing = await prisma.omniContactIdentity.findFirst({ + where: { + teamId: input.teamId, + channel: "TELEGRAM", + externalId: input.externalContactId, + }, + select: { contactId: true }, + }); + + if (existing?.contactId) { + return existing.contactId; + } + + const contact = await prisma.contact.create({ + data: { + teamId: input.teamId, + name: input.displayName, + avatarUrl: input.avatarUrl, + }, + select: { id: true }, + }); + + try { + await prisma.omniContactIdentity.create({ + data: { + teamId: input.teamId, + contactId: contact.id, + channel: "TELEGRAM", + externalId: input.externalContactId, + }, + }); + return contact.id; + } catch { + const concurrent = await prisma.omniContactIdentity.findFirst({ + where: { + teamId: input.teamId, + channel: "TELEGRAM", + externalId: input.externalContactId, + }, + select: { contactId: true }, + }); + if (concurrent?.contactId) { + await prisma.contact.delete({ where: { id: contact.id } }).catch(() => undefined); + return concurrent.contactId; + } + + throw new Error("failed to create telegram contact identity"); + } +} + +async function upsertThread(input: { + teamId: string; + contactId: string; + externalChatId: string; + businessConnectionId: string | null; + title: string | null; +}) { + const existing = await prisma.omniThread.findFirst({ + where: { + teamId: input.teamId, + channel: "TELEGRAM", + externalChatId: input.externalChatId, + businessConnectionId: input.businessConnectionId, + }, + select: { id: true }, + }); + + if (existing) { + await prisma.omniThread.update({ + where: { id: existing.id }, + data: { + contactId: input.contactId, + ...(input.title ? { title: input.title } : {}), + }, + select: { id: true }, + }); + return existing.id; + } + + try { + const created = await prisma.omniThread.create({ + data: { + teamId: input.teamId, + contactId: input.contactId, + channel: "TELEGRAM", + externalChatId: input.externalChatId, + businessConnectionId: input.businessConnectionId, + title: input.title, + }, + select: { id: true }, + }); + return created.id; + } catch { + const concurrent = await prisma.omniThread.findFirst({ + where: { + teamId: input.teamId, + channel: "TELEGRAM", + externalChatId: input.externalChatId, + businessConnectionId: input.businessConnectionId, + }, + select: { id: true }, + }); + + if (concurrent?.id) return concurrent.id; + throw new Error("failed to upsert telegram thread"); + } +} + +async function upsertContactInbox(input: { + teamId: string; + contactId: string; + sourceExternalId: string; + title: string | null; +}) { + const inbox = await prisma.contactInbox.upsert({ + where: { + teamId_channel_sourceExternalId: { + teamId: input.teamId, + channel: "TELEGRAM", + sourceExternalId: input.sourceExternalId, + }, + }, + create: { + teamId: input.teamId, + contactId: input.contactId, + channel: "TELEGRAM", + sourceExternalId: input.sourceExternalId, + title: input.title, + }, + update: { + contactId: input.contactId, + ...(input.title ? { title: input.title } : {}), + }, + select: { id: true }, + }); + + return inbox.id; +} + +async function markRead(teamId: string, externalChatId: string) { + const thread = await prisma.omniThread.findFirst({ + where: { + teamId, + channel: "TELEGRAM", + externalChatId, + }, + select: { contactId: true }, + }); + if (!thread) return; + + const members = await prisma.teamMember.findMany({ + where: { teamId }, + select: { userId: true }, + }); + + const readAt = new Date(); + await Promise.all( + members.map((member: { userId: string }) => + prisma.contactThreadRead.upsert({ + where: { + userId_contactId: { + userId: member.userId, + contactId: thread.contactId, + }, + }, + create: { + teamId, + userId: member.userId, + contactId: thread.contactId, + readAt, + }, + update: { readAt }, + }), + ), + ); +} + +export async function ingestTelegramInbound(envelope: TelegramInboundEnvelope) { + if (envelope.channel !== "TELEGRAM") { + return { ok: true, message: "skip_non_telegram" }; + } + + const teamId = await resolveTeamId(envelope); + if (!teamId) { + throw new Error("team_not_resolved"); + } + + const n = envelope.payloadNormalized; + const externalChatId = asString(n.threadExternalId) ?? asString(n.contactExternalId); + if (!externalChatId) { + throw new Error("thread_external_id_required"); + } + + if (envelope.eventType === "read_business_message") { + await markRead(teamId, externalChatId); + return { ok: true, message: "read_marked" }; + } + + const externalContactId = asString(n.contactExternalId) ?? externalChatId; + const businessConnectionId = asString(n.businessConnectionId); + const text = asString(n.text) ?? "[no text]"; + const occurredAt = parseDate(envelope.occurredAt); + const direction = normalizeDirection(envelope.direction); + + const contactFirstName = asString(n.contactFirstName); + const contactLastName = asString(n.contactLastName); + const contactUsername = asString(n.contactUsername); + const fallbackName = `Telegram ${externalContactId}`; + const displayName = + [contactFirstName, contactLastName].filter(Boolean).join(" ") || + (contactUsername ? `@${contactUsername.replace(/^@/, "")}` : null) || + fallbackName; + + const contactId = await resolveContact({ + teamId, + externalContactId, + displayName, + avatarUrl: asString(n.contactAvatarUrl), + }); + + const threadId = await upsertThread({ + teamId, + contactId, + externalChatId, + businessConnectionId, + title: asString(n.chatTitle), + }); + + const contactInboxId = await upsertContactInbox({ + teamId, + contactId, + sourceExternalId: externalChatId, + title: asString(n.chatTitle), + }); + + const rawEnvelope: Record = { + version: envelope.version, + source: "backend.graphql.ingestTelegramInbound", + provider: envelope.provider, + channel: envelope.channel, + direction, + providerEventId: envelope.providerEventId, + receivedAt: envelope.receivedAt, + occurredAt: occurredAt.toISOString(), + payloadNormalized: n, + payloadRaw: envelope.payloadRaw ?? null, + }; + + let omniMessageId: string; + if (envelope.providerMessageId) { + const message = await prisma.omniMessage.upsert({ + where: { + threadId_providerMessageId: { + threadId, + providerMessageId: envelope.providerMessageId, + }, + }, + create: { + teamId, + contactId, + threadId, + direction, + channel: "TELEGRAM", + status: "DELIVERED", + text, + providerMessageId: envelope.providerMessageId, + providerUpdateId: envelope.providerEventId, + rawJson: rawEnvelope, + occurredAt, + }, + update: { + text, + providerUpdateId: envelope.providerEventId, + rawJson: rawEnvelope, + occurredAt, + }, + select: { id: true }, + }); + omniMessageId = message.id; + } else { + const message = await prisma.omniMessage.create({ + data: { + teamId, + contactId, + threadId, + direction, + channel: "TELEGRAM", + status: "DELIVERED", + text, + providerMessageId: null, + providerUpdateId: envelope.providerEventId, + rawJson: rawEnvelope, + occurredAt, + }, + select: { id: true }, + }); + omniMessageId = message.id; + } + + await prisma.contactMessage.create({ + data: { + contactId, + contactInboxId, + kind: "MESSAGE", + direction, + channel: "TELEGRAM", + content: text, + occurredAt, + }, + }); + + return { ok: true, message: "inbound_ingested", omniMessageId }; +} + +export async function reportTelegramOutbound(input: TelegramOutboundReport) { + const statusRaw = input.status.trim().toUpperCase(); + const status: OmniMessageStatus = + statusRaw === "SENT" || + statusRaw === "FAILED" || + statusRaw === "DELIVERED" || + statusRaw === "READ" || + statusRaw === "PENDING" + ? (statusRaw as OmniMessageStatus) + : "FAILED"; + + const existing = await prisma.omniMessage.findUnique({ + where: { id: input.omniMessageId }, + select: { rawJson: true }, + }); + + const raw = (existing?.rawJson && typeof existing.rawJson === "object" && !Array.isArray(existing.rawJson) + ? (existing.rawJson as Record) + : {}) as Record; + + await prisma.omniMessage.update({ + where: { id: input.omniMessageId }, + data: { + status, + ...(input.providerMessageId ? { providerMessageId: input.providerMessageId } : {}), + rawJson: { + ...raw, + telegramWorker: { + reportedAt: new Date().toISOString(), + status, + error: input.error ?? null, + response: (() => { + if (!input.responseJson) return null; + try { + return JSON.parse(input.responseJson); + } catch { + return input.responseJson; + } + })(), + }, + }, + }, + }); + + return { ok: true, message: "outbound_reported" }; +} + +async function callTelegramBackendGraphql(query: string, variables: Record) { + const url = asString(process.env.TELEGRAM_BACKEND_GRAPHQL_URL); + if (!url) { + throw new Error("TELEGRAM_BACKEND_GRAPHQL_URL is required"); + } + + const headers: Record = { + "content-type": "application/json", + }; + + const secret = asString(process.env.TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET); + if (secret) { + headers["x-graphql-secret"] = secret; + } + + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ query, variables }), + }); + + const payload = (await response.json()) as { data?: T; errors?: Array<{ message?: string }> }; + if (!response.ok || payload.errors?.length) { + const errorMessage = payload.errors?.map((e) => e.message).filter(Boolean).join("; ") || `HTTP ${response.status}`; + throw new Error(errorMessage); + } + + return payload.data as T; +} + +export async function requestTelegramOutbound(input: TelegramOutboundRequest) { + type Out = { + enqueueTelegramOutbound: { + ok: boolean; + message: string; + runId?: string | null; + }; + }; + + const query = `mutation Enqueue($input: TelegramOutboundTaskInput!) { + enqueueTelegramOutbound(input: $input) { + ok + message + runId + } + }`; + + const data = await callTelegramBackendGraphql(query, { input }); + const result = data.enqueueTelegramOutbound; + if (!result?.ok) { + throw new Error(result?.message || "enqueue failed"); + } + + return { ok: true, message: "outbound_enqueued", runId: result.runId ?? null }; +} diff --git a/omni_chat/src/utils/prisma.ts b/backend/src/utils/prisma.ts similarity index 100% rename from omni_chat/src/utils/prisma.ts rename to backend/src/utils/prisma.ts diff --git a/omni_chat/tsconfig.json b/backend/tsconfig.json similarity index 100% rename from omni_chat/tsconfig.json rename to backend/tsconfig.json diff --git a/deploy-map.toml b/deploy-map.toml index 0a6740b..0ba56c0 100644 --- a/deploy-map.toml +++ b/deploy-map.toml @@ -2,8 +2,7 @@ version = 1 [services] frontend = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } -schedulers = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } -omni_outbound = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } -omni_inbound = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } -omni_chat = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } -langfuse = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui", compose_path = "langfuse/docker-compose.yml" } +backend = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } +telegram_backend = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } +telegram_worker = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" } +hatchet = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui", compose_path = "hatchet/docker-compose.yml" } diff --git a/docs/adr/0001-chat-platform-service-boundaries.md b/docs/adr/0001-chat-platform-service-boundaries.md index b906e13..b483f9c 100644 --- a/docs/adr/0001-chat-platform-service-boundaries.md +++ b/docs/adr/0001-chat-platform-service-boundaries.md @@ -1,104 +1,93 @@ -# ADR-0001: Разделение Chat Platform на 3 сервиса +# ADR-0001: Chat Platform Boundaries (GraphQL + Hatchet) -Дата: 2026-02-21 +Дата: 2026-03-08 Статус: accepted ## Контекст -Сейчас delivery уже вынесен отдельно, но часть omni-интеграции остается в приложении `frontend`. -Нужна архитектура, где входящие вебхуки, доменная логика чатов и исходящая доставка развиваются независимо и не ломают друг друга. +Нужна минимальная и предсказуемая схема из 5 сервисов: -Критичные требования: +- `frontend` +- `backend` +- `telegram_backend` +- `telegram_worker` +- `hatchet` -- входящие webhook-события не теряются при рестартах; -- delivery управляет retry/rate-limit централизованно; -- omni_chat остается единственным местом доменной логики и хранения состояния диалогов; -- сервисы можно обновлять независимо. +Ключевые ограничения: + +- основная Prisma/доменная БД только в `backend`; +- `telegram_backend` и `telegram_worker` не содержат CRM-домен и не пишут в основную БД; +- взаимодействие между сервисами только через GraphQL; +- асинхронность и ретраи централизованы в Hatchet. ## Решение -Принимаем разделение на 3 сервиса: +Принимаем архитектуру: -1. `omni_inbound` -- Принимает вебхуки провайдеров. -- Валидирует подпись/секрет. -- Нормализует событие в универсальный envelope. -- Пишет событие в durable queue (`receiver.flow`) с идемпотентным `jobId`. -- Возвращает `200` только после успешной durable enqueue. -- Не содержит бизнес-логики CRM. +1. `backend` +- владеет доменной моделью чатов и единственной основной Prisma-базой; +- принимает inbound события от `telegram_worker` через GraphQL (`ingestTelegramInbound`); +- создает outbound задачи в `telegram_backend` через GraphQL (`requestTelegramOutbound`); +- принимает delivery-отчеты от `telegram_worker` через GraphQL (`reportTelegramOutbound`). -2. `omni_chat` -- Потребляет входящие события из `receiver.flow`. -- Разрешает идентичности и треды. -- Создает/обновляет `OmniMessage`, `OmniThread`, статусы и доменные эффекты. -- Формирует исходящие команды и кладет их в `sender.flow`. +2. `telegram_backend` +- принимает webhook Telegram; +- нормализует payload в `OmniInboundEnvelopeV1`; +- ставит задачи в Hatchet (`process-telegram-inbound`, `process-telegram-outbound`); +- предоставляет GraphQL API для enqueue и отправки в Telegram API. -3. `omni_outbound` -- Потребляет `sender.flow`. -- Выполняет отправку в провайдеров (Telegram Business и др.). -- Управляет retry/backoff/failover, DLQ и статусами доставки. -- Не содержит UI и доменной логики чатов. +3. `telegram_worker` +- исполняет задачи Hatchet; +- для inbound вызывает `backend /graphql`; +- для outbound вызывает `telegram_backend /graphql` (`sendTelegramMessage`), затем `backend /graphql` (`reportTelegramOutbound`); +- не имеет собственной Prisma-базы. -## Почему webhook и delivery разделены +4. `hatchet` +- единый оркестратор задач, ретраев и backoff-политик. -- Входящий контур должен отвечать быстро и предсказуемо. -- Исходящий контур живет с долгими retry и ограничениями провайдера. -- Сбой внешнего API не должен блокировать прием входящих сообщений. +## Потоки + +### Inbound (Telegram -> CRM) + +1. Telegram webhook приходит в `telegram_backend`. +2. `telegram_backend` нормализует событие и enqueue в Hatchet `process-telegram-inbound`. +3. `telegram_worker` исполняет задачу и вызывает `backend.ingestTelegramInbound`. +4. `backend` сохраняет доменные изменения в своей БД. + +### Outbound (CRM -> Telegram) + +1. `backend` инициирует отправку (`requestTelegramOutbound`) в `telegram_backend`. +2. `telegram_backend` enqueue в Hatchet `process-telegram-outbound`. +3. `telegram_worker` вызывает `telegram_backend.sendTelegramMessage`. +4. `telegram_worker` репортит итог в `backend.reportTelegramOutbound`. ## Границы ответственности -`omni_inbound`: +`backend`: +- можно: вся бизнес-логика и состояние; +- нельзя: прямой вызов Telegram API. -- можно: auth, валидация, нормализация, дедуп, enqueue; -- нельзя: запись доменных сущностей CRM, принятие продуктовых решений. +`telegram_backend`: +- можно: webhook ingress, нормализация, enqueue, адаптер Telegram API; +- нельзя: доменные записи CRM. -`omni_chat`: +`telegram_worker`: +- можно: исполнение задач, ретраи, orchestration шагов; +- нельзя: хранение CRM-состояния и прямой доступ к основной БД. -- можно: вся доменная модель чатов, orchestration, бизнес-правила; -- нельзя: прямые вызовы провайдеров из sync API-контекста. +## Надежность -`omni_outbound`: - -- можно: провайдерные адаптеры, retry, rate limits; -- нельзя: резолвинг бизнес-правил и маршрутизации диалога. - -## Универсальный протокол событий - -Внутренний контракт входящих событий: `docs/contracts/omni-inbound-envelope.v1.json`. - -Обязательные поля: - -- `version` -- `idempotencyKey` -- `provider`, `channel`, `direction` -- `providerEventId`, `providerMessageId` -- `eventType`, `occurredAt`, `receivedAt` -- `payloadRaw`, `payloadNormalized` - -## Идемпотентность и надежность - -- `jobId` в очереди строится из `idempotencyKey`. -- Дубликаты входящих webhook событий безопасны и возвращают `200`. -- `200` от `omni_inbound` отдается только после успешного добавления в Redis/BullMQ. -- При ошибке durable enqueue `omni_inbound` возвращает `5xx`, провайдер выполняет повторную доставку. -- Базовые рабочие очереди: `receiver.flow` и `sender.flow`; технические очереди для эскалации: `receiver.retry`, `sender.retry`, `receiver.dlq`, `sender.dlq`. +- webhook отвечает `200` только после успешной постановки задачи в Hatchet; +- при недоступности сервисов задача ретраится Hatchet; +- inbound обработка идемпотентна через `idempotencyKey` и provider identifiers в `backend`. ## Последствия Плюсы: - -- независимые релизы и масштабирование по ролям; -- меньше blast radius при инцидентах; -- проще подключать новые каналы поверх общего контракта. +- меньше сервисов и меньше скрытых связей; +- изоляция доменной БД в `backend`; +- единая точка ретраев/оркестрации (Hatchet). Минусы: - -- больше инфраструктурных компонентов (очереди, мониторинг, трассировка); -- требуется дисциплина по контрактам между сервисами. - -## План внедрения - -1. Вводим `omni_inbound` как отдельный сервис для Telegram Business. -2. Потребление `receiver.flow` реализуем в `omni_chat`. -3. Текущее исходящее API оставляем за `omni_outbound`. -4. После стабилизации выносим оставшиеся omni endpoint'ы из `frontend` в `omni_chat`/`omni_inbound`. +- выше требования к стабильности GraphQL-контрактов между сервисами; +- нужна наблюдаемость по цепочке `telegram_backend -> hatchet -> telegram_worker -> backend`. diff --git a/docs/prisma-governance.md b/docs/prisma-governance.md index 31085ea..44178a5 100644 --- a/docs/prisma-governance.md +++ b/docs/prisma-governance.md @@ -2,19 +2,18 @@ ## Single source of truth -- Canonical Prisma schema: `frontend/prisma/schema.prisma`. -- Service copies: - - `omni_chat/prisma/schema.prisma` - - `omni_outbound/prisma/schema.prisma` +- Canonical Prisma schema: `Frontend/prisma/schema.prisma`. +- Service copy: + - `backend/prisma/schema.prisma` ## Update flow -1. Edit only `frontend/prisma/schema.prisma`. +1. Edit only `Frontend/prisma/schema.prisma`. 2. Run `./scripts/prisma-sync.sh`. 3. Run `./scripts/prisma-check.sh`. -4. Commit changed schema copies. +4. Commit changed schema copy. ## Rollout policy -- Schema rollout (`prisma db push` / migrations) is allowed only in `frontend`. -- `omni_chat` and `omni_outbound` must use generated Prisma client only. +- Schema rollout (`prisma db push` / migrations) is allowed only in `Frontend`. +- `backend` must use generated Prisma client only. diff --git a/frontend/server/api/omni/delivery/enqueue.post.ts b/frontend/server/api/omni/delivery/enqueue.post.ts index 892e1f9..40d80d7 100644 --- a/frontend/server/api/omni/delivery/enqueue.post.ts +++ b/frontend/server/api/omni/delivery/enqueue.post.ts @@ -1,60 +1,6 @@ -import { readBody } from "h3"; -import { getAuthContext } from "../../../utils/auth"; -import { prisma } from "../../../utils/prisma"; -import { enqueueOutboundDelivery } from "../../../queues/outboundDelivery"; - -type EnqueueBody = { - omniMessageId?: string; - endpoint?: string; - method?: "POST" | "PUT" | "PATCH"; - headers?: Record; - payload?: unknown; - provider?: string; - channel?: string; - attempts?: number; -}; - export default defineEventHandler(async (event) => { - const auth = await getAuthContext(event); - const body = await readBody(event); - - const omniMessageId = String(body?.omniMessageId ?? "").trim(); - const endpoint = String(body?.endpoint ?? "").trim(); - if (!omniMessageId) { - throw createError({ statusCode: 400, statusMessage: "omniMessageId is required" }); - } - if (!endpoint) { - throw createError({ statusCode: 400, statusMessage: "endpoint is required" }); - } - - const msg = await prisma.omniMessage.findFirst({ - where: { id: omniMessageId, teamId: auth.teamId }, - select: { id: true }, + throw createError({ + statusCode: 410, + statusMessage: "Legacy delivery enqueue is disabled. Use backend GraphQL requestTelegramOutbound.", }); - if (!msg) { - throw createError({ statusCode: 404, statusMessage: "omni message not found" }); - } - - const attempts = Math.max(1, Math.min(Number(body?.attempts ?? 12), 50)); - const job = await enqueueOutboundDelivery( - { - omniMessageId, - endpoint, - method: body?.method ?? "POST", - headers: body?.headers ?? {}, - payload: body?.payload ?? {}, - provider: body?.provider ?? undefined, - channel: body?.channel ?? undefined, - }, - { - attempts, - }, - ); - - return { - ok: true, - queue: process.env.SENDER_FLOW_QUEUE_NAME || process.env.OUTBOUND_DELIVERY_QUEUE_NAME || "sender.flow", - jobId: job.id, - omniMessageId, - }; }); diff --git a/frontend/server/api/omni/telegram/send.post.ts b/frontend/server/api/omni/telegram/send.post.ts index dcb32e1..d2d994e 100644 --- a/frontend/server/api/omni/telegram/send.post.ts +++ b/frontend/server/api/omni/telegram/send.post.ts @@ -1,11 +1,79 @@ import { readBody } from "h3"; import { getAuthContext } from "../../../utils/auth"; import { prisma } from "../../../utils/prisma"; -import { enqueueTelegramSend } from "../../../queues/telegramSend"; + +type BackendGraphqlResponse = { + data?: T; + errors?: Array<{ message?: string }>; +}; + +function asString(value: unknown) { + if (typeof value !== "string") return null; + const v = value.trim(); + return v || null; +} + +async function requestTelegramOutbound(input: { + omniMessageId: string; + chatId: string; + text: string; + businessConnectionId?: string | null; +}) { + type Out = { + requestTelegramOutbound: { + ok: boolean; + message: string; + runId?: string | null; + }; + }; + + const url = asString(process.env.BACKEND_GRAPHQL_URL); + if (!url) throw new Error("BACKEND_GRAPHQL_URL is required"); + + const headers: Record = { + "content-type": "application/json", + }; + const secret = asString(process.env.BACKEND_GRAPHQL_SHARED_SECRET); + if (secret) { + headers["x-graphql-secret"] = secret; + } + + const query = `mutation RequestTelegramOutbound($input: TelegramOutboundTaskInput!) { + requestTelegramOutbound(input: $input) { + ok + message + runId + } + }`; + + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + operationName: "RequestTelegramOutbound", + query, + variables: { input }, + }), + }); + + const payload = (await response.json()) as BackendGraphqlResponse; + if (!response.ok || payload.errors?.length) { + const message = + payload.errors?.map((error) => error.message).filter(Boolean).join("; ") || `HTTP ${response.status}`; + throw new Error(message); + } + + const result = payload.data?.requestTelegramOutbound; + if (!result?.ok) { + throw new Error(result?.message || "requestTelegramOutbound failed"); + } + + return result; +} export default defineEventHandler(async (event) => { const auth = await getAuthContext(event); - const body = await readBody<{ omniMessageId?: string; attempts?: number }>(event); + const body = await readBody<{ omniMessageId?: string }>(event); const omniMessageId = String(body?.omniMessageId ?? "").trim(); if (!omniMessageId) { @@ -14,19 +82,26 @@ export default defineEventHandler(async (event) => { const msg = await prisma.omniMessage.findFirst({ where: { id: omniMessageId, teamId: auth.teamId, channel: "TELEGRAM", direction: "OUT" }, - select: { id: true }, + select: { + id: true, + text: true, + thread: { select: { externalChatId: true, businessConnectionId: true } }, + }, }); if (!msg) { throw createError({ statusCode: 404, statusMessage: "telegram outbound message not found" }); } - const attempts = Math.max(1, Math.min(Number(body?.attempts ?? 12), 50)); - const job = await enqueueTelegramSend({ omniMessageId }, { attempts }); + const result = await requestTelegramOutbound({ + omniMessageId: msg.id, + chatId: msg.thread.externalChatId, + text: msg.text, + businessConnectionId: msg.thread.businessConnectionId ?? null, + }); return { ok: true, - queue: process.env.SENDER_FLOW_QUEUE_NAME || process.env.OUTBOUND_DELIVERY_QUEUE_NAME || "sender.flow", - jobId: job.id, + runId: result.runId ?? null, omniMessageId, }; }); diff --git a/frontend/server/disabled_plugins/queues.ts b/frontend/server/disabled_plugins/queues.ts index 59fb42f..a7a89d6 100644 --- a/frontend/server/disabled_plugins/queues.ts +++ b/frontend/server/disabled_plugins/queues.ts @@ -1,9 +1,4 @@ -import { startTelegramSendWorker } from "../queues/telegramSend"; - export default defineNitroPlugin(() => { - // Disabled by default. If you need background processing, wire it explicitly. - if (process.env.RUN_QUEUE_WORKER !== "1") return; - - startTelegramSendWorker(); + // Legacy BullMQ worker was removed. + // Delivery orchestration now runs in hatchet/telegram_worker service. }); - diff --git a/frontend/server/graphql/schema.ts b/frontend/server/graphql/schema.ts index acc05df..3afd9e8 100644 --- a/frontend/server/graphql/schema.ts +++ b/frontend/server/graphql/schema.ts @@ -8,7 +8,6 @@ import { normalizePhone, verifyPassword } from "../utils/password"; import { persistAiMessage, runCrmAgentFor } from "../agent/crmAgent"; import { buildChangeSet, captureSnapshot, rollbackChangeSet, rollbackChangeSetItems } from "../utils/changeSet"; import type { ChangeSet } from "../utils/changeSet"; -import { enqueueTelegramSend } from "../queues/telegramSend"; import { datasetRoot } from "../dataset/paths"; type GraphQLContext = { @@ -16,6 +15,11 @@ type GraphQLContext = { event: H3Event; }; +type BackendGraphqlResponse = { + data?: T; + errors?: Array<{ message?: string }>; +}; + function requireAuth(auth: AuthContext | null) { if (!auth) { throw new Error("Unauthorized"); @@ -45,6 +49,79 @@ function asObject(value: unknown): Record { return value as Record; } +function asString(value: unknown) { + if (typeof value !== "string") return null; + const v = value.trim(); + return v || null; +} + +async function requestTelegramOutboundFromBackend(input: { + omniMessageId: string; + chatId: string; + text: string; + businessConnectionId?: string | null; +}) { + type Out = { + requestTelegramOutbound: { + ok: boolean; + message: string; + runId?: string | null; + }; + }; + + const url = asString(process.env.BACKEND_GRAPHQL_URL); + if (!url) { + throw new Error("BACKEND_GRAPHQL_URL is required"); + } + + const headers: Record = { + "content-type": "application/json", + }; + + const secret = asString(process.env.BACKEND_GRAPHQL_SHARED_SECRET); + if (secret) { + headers["x-graphql-secret"] = secret; + } + + const query = `mutation RequestTelegramOutbound($input: TelegramOutboundTaskInput!) { + requestTelegramOutbound(input: $input) { + ok + message + runId + } + }`; + + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + operationName: "RequestTelegramOutbound", + query, + variables: { + input: { + omniMessageId: input.omniMessageId, + chatId: input.chatId, + text: input.text, + businessConnectionId: input.businessConnectionId ?? null, + }, + }, + }), + }); + + const payload = (await response.json()) as BackendGraphqlResponse; + if (!response.ok || payload.errors?.length) { + const message = payload.errors?.map((error) => error.message).filter(Boolean).join("; ") || `HTTP ${response.status}`; + throw new Error(message); + } + + const result = payload.data?.requestTelegramOutbound; + if (!result?.ok) { + throw new Error(result?.message || "requestTelegramOutbound failed"); + } + + return result; +} + function readNestedString(obj: Record, path: string[]): string { let current: unknown = obj; for (const segment of path) { @@ -1416,7 +1493,7 @@ async function createCommunication(auth: AuthContext | null, input: { channel: "TELEGRAM", }, orderBy: { updatedAt: "desc" }, - select: { id: true, externalChatId: true, title: true }, + select: { id: true, externalChatId: true, businessConnectionId: true, title: true }, }); if (!thread) { throw new Error("telegram thread not found for contact"); @@ -1464,7 +1541,12 @@ async function createCommunication(auth: AuthContext | null, input: { }); try { - await enqueueTelegramSend({ omniMessageId: omniMessage.id }); + await requestTelegramOutboundFromBackend({ + omniMessageId: omniMessage.id, + chatId: thread.externalChatId, + text: content, + businessConnectionId: thread.businessConnectionId ?? null, + }); } catch (error) { const message = error instanceof Error ? error.message : String(error); const existingOmni = await prisma.omniMessage.findUnique({ @@ -1486,7 +1568,7 @@ async function createCommunication(auth: AuthContext | null, input: { }, }, }).catch(() => undefined); - throw new Error(`telegram enqueue failed: ${message}`); + throw new Error(`telegram outbound request failed: ${message}`); } } else { const existingInbox = await prisma.contactInbox.findFirst({ diff --git a/frontend/server/queues/outboundDelivery.ts b/frontend/server/queues/outboundDelivery.ts deleted file mode 100644 index 5b046cd..0000000 --- a/frontend/server/queues/outboundDelivery.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { Queue, Worker, type JobsOptions, type ConnectionOptions } from "bullmq"; -import { Prisma } from "../generated/prisma/client"; -import { prisma } from "../utils/prisma"; - -export const OUTBOUND_DELIVERY_QUEUE_NAME = ( - process.env.SENDER_FLOW_QUEUE_NAME || - process.env.OUTBOUND_DELIVERY_QUEUE_NAME || - "sender.flow" -).trim(); - -export type OutboundDeliveryJob = { - omniMessageId: string; - endpoint: string; - method?: "POST" | "PUT" | "PATCH"; - headers?: Record; - payload: unknown; - channel?: string; - provider?: string; -}; - -function redisConnectionFromEnv(): ConnectionOptions { - const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim(); - const parsed = new URL(raw); - return { - host: parsed.hostname, - port: parsed.port ? Number(parsed.port) : 6379, - username: parsed.username ? decodeURIComponent(parsed.username) : undefined, - password: parsed.password ? decodeURIComponent(parsed.password) : undefined, - db: parsed.pathname && parsed.pathname !== "/" ? Number(parsed.pathname.slice(1)) : undefined, - maxRetriesPerRequest: null, - }; -} - -function ensureHttpUrl(value: string) { - const raw = (value ?? "").trim(); - if (!raw) throw new Error("endpoint is required"); - const parsed = new URL(raw); - if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { - throw new Error(`Unsupported endpoint protocol: ${parsed.protocol}`); - } - return parsed.toString(); -} - -function compactError(error: unknown) { - if (!error) return "unknown_error"; - if (typeof error === "string") return error; - const anyErr = error as any; - return String(anyErr?.message ?? anyErr); -} - -function extractProviderMessageId(body: unknown): string | null { - const obj = body as any; - if (!obj || typeof obj !== "object") return null; - const candidate = - obj?.message_id ?? - obj?.messageId ?? - obj?.id ?? - obj?.result?.message_id ?? - obj?.result?.id ?? - null; - if (candidate == null) return null; - return String(candidate); -} - -function asObject(value: unknown): Record { - if (!value || typeof value !== "object" || Array.isArray(value)) return {}; - return value as Record; -} - -export function outboundDeliveryQueue() { - return new Queue(OUTBOUND_DELIVERY_QUEUE_NAME, { - connection: redisConnectionFromEnv(), - defaultJobOptions: { - removeOnComplete: { count: 1000 }, - removeOnFail: { count: 5000 }, - }, - }); -} - -export async function enqueueOutboundDelivery(input: OutboundDeliveryJob, opts?: JobsOptions) { - const endpoint = ensureHttpUrl(input.endpoint); - const q = outboundDeliveryQueue(); - - const payload = (input.payload ?? null) as Prisma.InputJsonValue; - const existing = await prisma.omniMessage.findUnique({ - where: { id: input.omniMessageId }, - select: { rawJson: true }, - }); - const raw = asObject(existing?.rawJson); - const rawQueue = asObject(raw.queue); - const rawDeliveryRequest = asObject(raw.deliveryRequest); - // Keep source message in pending before actual send starts. - await prisma.omniMessage.update({ - where: { id: input.omniMessageId }, - data: { - status: "PENDING", - rawJson: { - ...raw, - queue: { - ...rawQueue, - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - enqueuedAt: new Date().toISOString(), - }, - deliveryRequest: { - ...rawDeliveryRequest, - endpoint, - method: input.method ?? "POST", - channel: input.channel ?? null, - provider: input.provider ?? null, - payload, - }, - }, - }, - }); - - return q.add("deliver", { ...input, endpoint }, { - jobId: `omni-${input.omniMessageId}`, - attempts: 12, - backoff: { type: "exponential", delay: 1000 }, - ...opts, - }); -} - -export function startOutboundDeliveryWorker() { - return new Worker( - OUTBOUND_DELIVERY_QUEUE_NAME, - async (job) => { - const msg = await prisma.omniMessage.findUnique({ - where: { id: job.data.omniMessageId }, - include: { thread: true }, - }); - if (!msg) return; - - // Idempotency: if already sent/delivered, do not resend. - if ((msg.status === "SENT" || msg.status === "DELIVERED" || msg.status === "READ") && msg.providerMessageId) { - return; - } - - const endpoint = ensureHttpUrl(job.data.endpoint); - const method = job.data.method ?? "POST"; - const headers: Record = { - "content-type": "application/json", - ...(job.data.headers ?? {}), - }; - - const requestPayload = (job.data.payload ?? null) as Prisma.InputJsonValue; - const requestStartedAt = new Date().toISOString(); - try { - const response = await fetch(endpoint, { - method, - headers, - body: JSON.stringify(requestPayload ?? {}), - }); - - const text = await response.text(); - const responseBody = (() => { - try { - return JSON.parse(text); - } catch { - return text; - } - })(); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody)}`); - } - - const providerMessageId = extractProviderMessageId(responseBody); - const raw = asObject(msg.rawJson); - const rawQueue = asObject(raw.queue); - const rawDeliveryRequest = asObject(raw.deliveryRequest); - await prisma.omniMessage.update({ - where: { id: msg.id }, - data: { - status: "SENT", - providerMessageId, - rawJson: { - ...raw, - queue: { - ...rawQueue, - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - completedAt: new Date().toISOString(), - attemptsMade: job.attemptsMade + 1, - }, - deliveryRequest: { - ...rawDeliveryRequest, - endpoint, - method, - channel: job.data.channel ?? null, - provider: job.data.provider ?? null, - startedAt: requestStartedAt, - payload: requestPayload, - }, - deliveryResponse: { - status: response.status, - body: responseBody, - }, - }, - }, - }); - } catch (error) { - const isLastAttempt = - typeof job.opts.attempts === "number" && job.attemptsMade + 1 >= job.opts.attempts; - - if (isLastAttempt) { - const raw = asObject(msg.rawJson); - const rawQueue = asObject(raw.queue); - const rawDeliveryRequest = asObject(raw.deliveryRequest); - await prisma.omniMessage.update({ - where: { id: msg.id }, - data: { - status: "FAILED", - rawJson: { - ...raw, - queue: { - ...rawQueue, - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - failedAt: new Date().toISOString(), - attemptsMade: job.attemptsMade + 1, - }, - deliveryRequest: { - ...rawDeliveryRequest, - endpoint, - method, - channel: job.data.channel ?? null, - provider: job.data.provider ?? null, - startedAt: requestStartedAt, - payload: requestPayload, - }, - deliveryError: { - message: compactError(error), - }, - }, - }, - }); - } - - throw error; - } - }, - { connection: redisConnectionFromEnv() }, - ); -} diff --git a/frontend/server/queues/telegramSend.ts b/frontend/server/queues/telegramSend.ts deleted file mode 100644 index ce1eac5..0000000 --- a/frontend/server/queues/telegramSend.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { JobsOptions } from "bullmq"; -import { prisma } from "../utils/prisma"; -import { telegramApiBase, requireTelegramBotToken } from "../utils/telegram"; -import { enqueueOutboundDelivery, startOutboundDeliveryWorker } from "./outboundDelivery"; - -type TelegramSendJob = { - omniMessageId: string; -}; - -function asObject(value: unknown): Record { - if (!value || typeof value !== "object" || Array.isArray(value)) return {}; - return value as Record; -} - -function readNestedString(obj: Record, path: string[]): string { - let current: unknown = obj; - for (const segment of path) { - if (!current || typeof current !== "object" || Array.isArray(current)) return ""; - current = (current as Record)[segment]; - } - return typeof current === "string" ? current.trim() : ""; -} - -export async function enqueueTelegramSend(input: TelegramSendJob, opts?: JobsOptions) { - const msg = await prisma.omniMessage.findUnique({ - where: { id: input.omniMessageId }, - include: { thread: true }, - }); - if (!msg) throw new Error(`omni message not found: ${input.omniMessageId}`); - if (msg.channel !== "TELEGRAM" || msg.direction !== "OUT") { - throw new Error(`Invalid omni message for telegram send: ${msg.id}`); - } - const raw = asObject(msg.rawJson); - const text = - readNestedString(raw, ["normalized", "text"]) || - readNestedString(raw, ["payloadNormalized", "text"]) || - msg.text; - if (!text) { - throw new Error(`Omni message has empty text payload: ${msg.id}`); - } - - const token = requireTelegramBotToken(); - const endpoint = `${telegramApiBase()}/bot${token}/sendMessage`; - const payload = { - chat_id: msg.thread.externalChatId, - text, - ...(msg.thread.businessConnectionId ? { business_connection_id: msg.thread.businessConnectionId } : {}), - }; - - return enqueueOutboundDelivery( - { - omniMessageId: msg.id, - endpoint, - method: "POST", - payload, - provider: "telegram_business", - channel: "TELEGRAM", - }, - opts, - ); -} - -export function startTelegramSendWorker() { - return startOutboundDeliveryWorker(); -} diff --git a/frontend/server/queues/worker.ts b/frontend/server/queues/worker.ts deleted file mode 100644 index d6430d2..0000000 --- a/frontend/server/queues/worker.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OUTBOUND_DELIVERY_QUEUE_NAME, startOutboundDeliveryWorker } from "./outboundDelivery"; -import { prisma } from "../utils/prisma"; -import { getRedis } from "../utils/redis"; - -const worker = startOutboundDeliveryWorker(); -console.log(`[omni_outbound(legacy-in-frontend)] started queue ${OUTBOUND_DELIVERY_QUEUE_NAME}`); - -async function shutdown(signal: string) { - console.log(`[omni_outbound(legacy-in-frontend)] shutting down by ${signal}`); - try { - await worker.close(); - } catch { - // ignore shutdown errors - } - try { - const redis = getRedis(); - await redis.quit(); - } catch { - // ignore shutdown errors - } - try { - await prisma.$disconnect(); - } catch { - // ignore shutdown errors - } - process.exit(0); -} - -process.on("SIGINT", () => { - void shutdown("SIGINT"); -}); -process.on("SIGTERM", () => { - void shutdown("SIGTERM"); -}); diff --git a/frontend/server/utils/redis.ts b/frontend/server/utils/redis.ts deleted file mode 100644 index 5b31e1f..0000000 --- a/frontend/server/utils/redis.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Redis from "ioredis"; - -declare global { - // eslint-disable-next-line no-var - var __redis: Redis | undefined; -} - -export function getRedis() { - if (globalThis.__redis) return globalThis.__redis; - - const url = process.env.REDIS_URL || "redis://localhost:6379"; - const client = new Redis(url, { - maxRetriesPerRequest: null, // recommended for BullMQ - }); - - if (process.env.NODE_ENV !== "production") { - globalThis.__redis = client; - } - - return client; -} - diff --git a/hatchet/README.md b/hatchet/README.md new file mode 100644 index 0000000..0ac91a5 --- /dev/null +++ b/hatchet/README.md @@ -0,0 +1,34 @@ +# Hatchet (Dokploy) + +Compose-стек для self-hosted Hatchet, перенесенный из соседнего проекта `gl` и адаптированный под ENV. + +## Файлы + +- `docker-compose.yml` — сервисы Hatchet (Postgres, RabbitMQ, migration, setup-config, engine, dashboard). + +## Обязательные ENV (Dokploy UI) + +- `HATCHET_POSTGRES_USER` (default: `hatchet`) +- `HATCHET_POSTGRES_PASSWORD` (default: `hatchet`) +- `HATCHET_POSTGRES_DB` (default: `hatchet`) +- `HATCHET_DATABASE_URL` (default: `postgres://hatchet:hatchet@postgres:5432/hatchet`) +- `HATCHET_RABBITMQ_USER` (default: `user`) +- `HATCHET_RABBITMQ_PASSWORD` (default: `password`) +- `HATCHET_RABBITMQ_URL` (default: `amqp://user:password@rabbitmq:5672/`) +- `HATCHET_SERVER_AUTH_COOKIE_DOMAIN` (например, `hatchet.<ваш-домен>`) +- `HATCHET_SERVER_AUTH_COOKIE_INSECURE` (`t`/`f`) +- `HATCHET_SERVER_GRPC_INSECURE` (`t`/`f`) +- `HATCHET_SERVER_GRPC_BROADCAST_ADDRESS` (например, `hatchet-engine:7070` внутри сети) + +## ENV для приложений-воркеров (Node SDK) + +- `HATCHET_CLIENT_TOKEN` — токен клиента из Hatchet. +- `HATCHET_CLIENT_TLS_STRATEGY` — для self-host без TLS: `none`. +- `HATCHET_CLIENT_HOST_PORT` — gRPC адрес (например, `hatchet-engine:7070` в одной Docker-сети). +- `HATCHET_CLIENT_API_URL` — URL API Hatchet dashboard/api. + +## Развертывание + +Сервис описан в `deploy-map.toml` как: + +`hatchet = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui", compose_path = "hatchet/docker-compose.yml" }` diff --git a/hatchet/docker-compose.yml b/hatchet/docker-compose.yml new file mode 100644 index 0000000..995a7c4 --- /dev/null +++ b/hatchet/docker-compose.yml @@ -0,0 +1,121 @@ +services: + postgres: + image: postgres:15.6 + command: postgres -c "max_connections=1000" + restart: always + hostname: postgres + environment: + POSTGRES_USER: ${HATCHET_POSTGRES_USER:-hatchet} + POSTGRES_PASSWORD: ${HATCHET_POSTGRES_PASSWORD:-hatchet} + POSTGRES_DB: ${HATCHET_POSTGRES_DB:-hatchet} + expose: + - "5432" + volumes: + - hatchet_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -d ${HATCHET_POSTGRES_DB:-hatchet} -U ${HATCHET_POSTGRES_USER:-hatchet}"] + interval: 10s + timeout: 10s + retries: 5 + start_period: 10s + + rabbitmq: + image: rabbitmq:3-management + hostname: rabbitmq + expose: + - "5672" + - "15672" + environment: + RABBITMQ_DEFAULT_USER: ${HATCHET_RABBITMQ_USER:-user} + RABBITMQ_DEFAULT_PASS: ${HATCHET_RABBITMQ_PASSWORD:-password} + volumes: + - hatchet_rabbitmq_data:/var/lib/rabbitmq + - hatchet_rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf + healthcheck: + test: ["CMD", "rabbitmqctl", "status"] + interval: 10s + timeout: 10s + retries: 5 + + migration: + image: ghcr.io/hatchet-dev/hatchet/hatchet-migrate:latest + command: /hatchet/hatchet-migrate + environment: + DATABASE_URL: ${HATCHET_DATABASE_URL:-postgres://hatchet:hatchet@postgres:5432/hatchet} + depends_on: + postgres: + condition: service_healthy + + setup-config: + image: ghcr.io/hatchet-dev/hatchet/hatchet-admin:latest + command: /hatchet/hatchet-admin quickstart --skip certs --generated-config-dir /hatchet/config --overwrite=false + environment: + DATABASE_URL: ${HATCHET_DATABASE_URL:-postgres://hatchet:hatchet@postgres:5432/hatchet} + SERVER_MSGQUEUE_RABBITMQ_URL: ${HATCHET_RABBITMQ_URL:-amqp://user:password@rabbitmq:5672/} + SERVER_AUTH_COOKIE_DOMAIN: ${HATCHET_SERVER_AUTH_COOKIE_DOMAIN:-hatchet.local} + SERVER_AUTH_COOKIE_INSECURE: ${HATCHET_SERVER_AUTH_COOKIE_INSECURE:-t} + SERVER_GRPC_BIND_ADDRESS: 0.0.0.0 + SERVER_GRPC_INSECURE: ${HATCHET_SERVER_GRPC_INSECURE:-t} + SERVER_GRPC_BROADCAST_ADDRESS: ${HATCHET_SERVER_GRPC_BROADCAST_ADDRESS:-hatchet-engine:7070} + SERVER_DEFAULT_ENGINE_VERSION: V1 + SERVER_INTERNAL_CLIENT_INTERNAL_GRPC_BROADCAST_ADDRESS: hatchet-engine:7070 + volumes: + - hatchet_certs:/hatchet/certs + - hatchet_config:/hatchet/config + depends_on: + migration: + condition: service_completed_successfully + rabbitmq: + condition: service_healthy + postgres: + condition: service_healthy + + hatchet-engine: + image: ghcr.io/hatchet-dev/hatchet/hatchet-engine:latest + command: /hatchet/hatchet-engine --config /hatchet/config + restart: on-failure + depends_on: + setup-config: + condition: service_completed_successfully + migration: + condition: service_completed_successfully + expose: + - "7070" + environment: + DATABASE_URL: ${HATCHET_DATABASE_URL:-postgres://hatchet:hatchet@postgres:5432/hatchet} + SERVER_GRPC_BIND_ADDRESS: 0.0.0.0 + SERVER_GRPC_INSECURE: ${HATCHET_SERVER_GRPC_INSECURE:-t} + volumes: + - hatchet_certs:/hatchet/certs + - hatchet_config:/hatchet/config + networks: + - default + - dokploy-network + + hatchet-dashboard: + image: ghcr.io/hatchet-dev/hatchet/hatchet-dashboard:latest + command: sh ./entrypoint.sh --config /hatchet/config + expose: + - "80" + restart: on-failure + depends_on: + setup-config: + condition: service_completed_successfully + migration: + condition: service_completed_successfully + environment: + DATABASE_URL: ${HATCHET_DATABASE_URL:-postgres://hatchet:hatchet@postgres:5432/hatchet} + volumes: + - hatchet_certs:/hatchet/certs + - hatchet_config:/hatchet/config + +networks: + dokploy-network: + external: true + +volumes: + hatchet_postgres_data: + hatchet_rabbitmq_data: + hatchet_rabbitmq.conf: + hatchet_config: + hatchet_certs: diff --git a/omni_chat/README.md b/omni_chat/README.md deleted file mode 100644 index 1b2a598..0000000 --- a/omni_chat/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# omni_chat - -Изолированный сервис chat-core (домен диалогов). - -## Назначение - -- потребляет входящие события из `receiver.flow`; -- применяет бизнес-логику диалогов; -- публикует исходящие команды в `sender.flow`. - -Текущий шаг: выделен отдельный сервисный контур и health endpoint. - -## API - -- `GET /health` - -## Переменные окружения - -- `PORT` (default: `8090`) -- `RECEIVER_FLOW_QUEUE_NAME` (default: `receiver.flow`) -- `SENDER_FLOW_QUEUE_NAME` (default: `sender.flow`) - -## Prisma policy - -- Источник схемы: `frontend/prisma/schema.prisma`. -- Локальная копия в `omni_chat/prisma/schema.prisma` обновляется только через `scripts/prisma-sync.sh`. -- Миграции/`db push` выполняются только в `frontend`. diff --git a/omni_chat/package.json b/omni_chat/package.json deleted file mode 100644 index 490c3a4..0000000 --- a/omni_chat/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "crm-omni-chat", - "private": true, - "type": "module", - "scripts": { - "start": "tsx src/index.ts", - "typecheck": "tsc --noEmit", - "postinstall": "node ./node_modules/prisma/build/index.js generate --schema ./prisma/schema.prisma" - }, - "devDependencies": { - "@types/node": "^22.13.9", - "prisma": "^6.16.1", - "tsx": "^4.20.5", - "typescript": "^5.9.2" - }, - "dependencies": { - "@prisma/client": "^6.16.1", - "bullmq": "^5.70.0" - } -} diff --git a/omni_chat/src/index.ts b/omni_chat/src/index.ts deleted file mode 100644 index b11390a..0000000 --- a/omni_chat/src/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createServer } from "node:http"; -import { closeReceiverWorker, RECEIVER_FLOW_QUEUE_NAME, receiverQueue, startReceiverWorker } from "./worker"; - -const port = Number(process.env.PORT || 8090); -const service = "omni_chat"; - -const server = createServer(async (req, res) => { - if (req.method === "GET" && req.url === "/health") { - const q = receiverQueue(); - const counts = await q.getJobCounts("wait", "active", "failed", "completed", "delayed"); - await q.close(); - const payload = JSON.stringify({ - ok: true, - service, - receiverFlow: RECEIVER_FLOW_QUEUE_NAME, - senderFlow: process.env.SENDER_FLOW_QUEUE_NAME || "sender.flow", - queue: counts, - now: new Date().toISOString(), - }); - res.statusCode = 200; - res.setHeader("content-type", "application/json; charset=utf-8"); - res.end(payload); - return; - } - - res.statusCode = 404; - res.setHeader("content-type", "application/json; charset=utf-8"); - res.end(JSON.stringify({ ok: false, error: "not_found" })); -}); - -startReceiverWorker(); - -server.listen(port, "0.0.0.0", () => { - console.log(`[omni_chat] listening on :${port}`); - console.log(`[omni_chat] receiver worker started for queue ${RECEIVER_FLOW_QUEUE_NAME}`); -}); - -async function shutdown(signal: string) { - console.log(`[omni_chat] shutting down by ${signal}`); - try { - await closeReceiverWorker(); - } finally { - server.close(() => process.exit(0)); - } -} - -process.on("SIGINT", () => { - void shutdown("SIGINT"); -}); -process.on("SIGTERM", () => { - void shutdown("SIGTERM"); -}); diff --git a/omni_chat/src/worker.ts b/omni_chat/src/worker.ts deleted file mode 100644 index 4d8836a..0000000 --- a/omni_chat/src/worker.ts +++ /dev/null @@ -1,719 +0,0 @@ -import { Queue, Worker, type ConnectionOptions, type Job } from "bullmq"; -import { Prisma } from "@prisma/client"; -import { prisma } from "./utils/prisma"; - -type JsonObject = Record; - -type OmniInboundEnvelopeV1 = { - version: 1; - idempotencyKey: string; - provider: string; - channel: "TELEGRAM" | "WHATSAPP" | "INSTAGRAM" | "PHONE" | "EMAIL" | "INTERNAL"; - direction: "IN" | "OUT"; - providerEventId: string; - providerMessageId: string | null; - eventType: string; - occurredAt: string; - receivedAt: string; - payloadRaw: unknown; - payloadNormalized: { - threadExternalId: string | null; - contactExternalId: string | null; - text: string | null; - businessConnectionId: string | null; - updateId?: string | null; - [key: string]: unknown; - }; -}; - -export const RECEIVER_FLOW_QUEUE_NAME = (process.env.RECEIVER_FLOW_QUEUE_NAME || "receiver.flow").trim(); -const TELEGRAM_PLACEHOLDER_PREFIX = "Telegram "; -const TELEGRAM_AUDIO_FILE_MARKER = "tg-file:"; -const TELEGRAM_WAVE_BINS = 96; -const TELEGRAM_AVATAR_FILE_MARKER = "tg-file:"; - -function redisConnectionFromEnv(): ConnectionOptions { - const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim(); - const parsed = new URL(raw); - return { - host: parsed.hostname, - port: parsed.port ? Number(parsed.port) : 6379, - username: parsed.username ? decodeURIComponent(parsed.username) : undefined, - password: parsed.password ? decodeURIComponent(parsed.password) : undefined, - db: parsed.pathname && parsed.pathname !== "/" ? Number(parsed.pathname.slice(1)) : undefined, - maxRetriesPerRequest: null, - }; -} - -function normalizeText(input: unknown) { - const t = String(input ?? "").trim(); - return t || "[no text]"; -} - -type TelegramInboundMedia = { - kind: "voice" | "audio" | "video_note" | null; - fileId: string | null; - durationSec: number | null; - label: string | null; -}; - -function parseTelegramInboundMedia(normalized: OmniInboundEnvelopeV1["payloadNormalized"]): TelegramInboundMedia { - const kindRaw = String(normalized.mediaKind ?? "").trim().toLowerCase(); - const kind: TelegramInboundMedia["kind"] = - kindRaw === "voice" || kindRaw === "audio" || kindRaw === "video_note" - ? kindRaw - : null; - - const fileId = asString(normalized.mediaFileId); - const durationRaw = normalized.mediaDurationSec; - const durationParsed = - typeof durationRaw === "number" - ? durationRaw - : typeof durationRaw === "string" - ? Number(durationRaw) - : Number.NaN; - const durationSec = - Number.isFinite(durationParsed) && durationParsed > 0 - ? Math.max(1, Math.round(durationParsed)) - : null; - - const label = asString(normalized.mediaTitle); - return { kind, fileId, durationSec, label }; -} - -function fallbackTextFromMedia(media: TelegramInboundMedia) { - if (!media.kind) return null; - if (media.kind === "voice") return "[voice message]"; - if (media.kind === "video_note") return "[video note]"; - if (media.label) return `[audio] ${media.label}`; - return "[audio]"; -} - -function buildFallbackWaveform(seedText: string, bins = TELEGRAM_WAVE_BINS) { - let seed = 0; - for (let i = 0; i < seedText.length; i += 1) { - seed = (seed * 33 + seedText.charCodeAt(i)) >>> 0; - } - - const random = () => { - seed = (seed * 1664525 + 1013904223) >>> 0; - return seed / 0xffffffff; - }; - - const out: number[] = []; - let smooth = 0; - for (let i = 0; i < bins; i += 1) { - const t = i / Math.max(1, bins - 1); - const burst = Math.max(0, Math.sin(t * Math.PI * (2 + (seedText.length % 5)))); - const noise = (random() * 2 - 1) * 0.6; - smooth = smooth * 0.72 + noise * 0.28; - const value = Math.max(0.06, Math.min(1, 0.12 + Math.abs(smooth) * 0.42 + burst * 0.4)); - out.push(Number(value.toFixed(4))); - } - return out; -} - -function buildWaveformFromBytes(bytes: Uint8Array, bins = TELEGRAM_WAVE_BINS) { - if (!bytes.length) return []; - const bucketSize = Math.max(1, Math.ceil(bytes.length / bins)); - const raw = new Array(bins).fill(0); - - for (let i = 0; i < bins; i += 1) { - const start = i * bucketSize; - const end = Math.min(bytes.length, start + bucketSize); - if (start >= end) continue; - - let energy = 0; - for (let j = start; j < end; j += 1) { - energy += Math.abs(bytes[j] - 128) / 128; - } - raw[i] = energy / (end - start); - } - - const smooth: number[] = []; - let prev = 0; - for (const value of raw) { - prev = prev * 0.78 + value * 0.22; - smooth.push(prev); - } - - const maxValue = Math.max(...smooth, 0); - if (maxValue <= 0) return []; - - return smooth.map((value) => { - const normalized = value / maxValue; - const mapped = Math.max(0.06, Math.min(1, normalized * 0.9 + 0.06)); - return Number(mapped.toFixed(4)); - }); -} - -async function fetchTelegramFileBytes(fileId: string) { - const token = String(process.env.TELEGRAM_BOT_TOKEN ?? "").trim(); - if (!token) return null; - - const base = String(process.env.TELEGRAM_API_BASE ?? "https://api.telegram.org").replace(/\/+$/, ""); - - const metaRes = await fetch(`${base}/bot${token}/getFile`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ file_id: fileId }), - }); - const metaJson = (await metaRes.json().catch(() => null)) as - | { ok?: boolean; result?: { file_path?: string } } - | null; - const filePath = String(metaJson?.result?.file_path ?? "").trim(); - if (!metaRes.ok || !metaJson?.ok || !filePath) return null; - - const fileRes = await fetch(`${base}/file/bot${token}/${filePath}`); - if (!fileRes.ok) return null; - return new Uint8Array(await fileRes.arrayBuffer()); -} - -type TelegramChatPhoto = { - small_file_id?: string; - big_file_id?: string; -}; - -type TelegramGetChatResponse = { - ok?: boolean; - result?: { - photo?: TelegramChatPhoto; - }; -}; - -type TelegramProfilePhotoSize = { - file_id?: string; -}; - -type TelegramGetUserProfilePhotosResponse = { - ok?: boolean; - result?: { - photos?: TelegramProfilePhotoSize[][]; - }; -}; - -function asTelegramAvatarUrl(fileId: string | null | undefined) { - const normalized = asString(fileId); - if (!normalized) return null; - return `${TELEGRAM_AVATAR_FILE_MARKER}${normalized}`; -} - -async function fetchTelegramAvatarUrl(externalContactId: string) { - const token = String(process.env.TELEGRAM_BOT_TOKEN ?? "").trim(); - if (!token) return null; - - const base = String(process.env.TELEGRAM_API_BASE ?? "https://api.telegram.org").replace(/\/+$/, ""); - - const getChatRes = await fetch(`${base}/bot${token}/getChat`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ chat_id: externalContactId }), - }); - const getChatJson = (await getChatRes.json()) as TelegramGetChatResponse; - if (getChatRes.ok && getChatJson.ok) { - const fromChat = asTelegramAvatarUrl( - getChatJson.result?.photo?.small_file_id ?? getChatJson.result?.photo?.big_file_id, - ); - if (fromChat) return fromChat; - } - - const getPhotosRes = await fetch(`${base}/bot${token}/getUserProfilePhotos`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ user_id: externalContactId, limit: 1 }), - }); - const getPhotosJson = (await getPhotosRes.json()) as TelegramGetUserProfilePhotosResponse; - if (!getPhotosRes.ok || !getPhotosJson.ok) return null; - - const firstPhotoSizes = Array.isArray(getPhotosJson.result?.photos?.[0]) - ? getPhotosJson.result?.photos?.[0] - : []; - const candidate = firstPhotoSizes.at(-1)?.file_id ?? firstPhotoSizes[0]?.file_id; - return asTelegramAvatarUrl(candidate); -} - -async function resolveInboundWaveform(media: TelegramInboundMedia, text: string) { - const fallback = buildFallbackWaveform(`${media.fileId ?? "none"}:${media.durationSec ?? "0"}:${text}`); - const fileId = media.fileId; - if (!fileId) return fallback; - - try { - const bytes = await fetchTelegramFileBytes(fileId); - if (!bytes?.length) return fallback; - const fromFile = buildWaveformFromBytes(bytes); - return fromFile.length ? fromFile : fallback; - } catch { - return fallback; - } -} - -function parseOccurredAt(input: string | null | undefined) { - const d = new Date(String(input ?? "")); - if (Number.isNaN(d.getTime())) return new Date(); - return d; -} - -function asString(input: unknown) { - if (typeof input !== "string") return null; - const trimmed = input.trim(); - return trimmed || null; -} - -function safeDirection(input: unknown): "IN" | "OUT" { - return input === "OUT" ? "OUT" : "IN"; -} - -function isUniqueConstraintError(error: unknown) { - return error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002"; -} - -type ContactProfile = { - displayName: string; - avatarUrl: string | null; -}; - -function buildContactProfile( - normalized: OmniInboundEnvelopeV1["payloadNormalized"], - externalContactId: string, -): ContactProfile { - // Use only normalized contact-* fields (counterparty), avoid sender/chat fallbacks - // to prevent accidental renames to the business owner name on OUT events. - const firstName = asString(normalized.contactFirstName); - const lastName = asString(normalized.contactLastName); - const username = asString(normalized.contactUsername); - const title = asString(normalized.contactTitle); - - const fullName = [firstName, lastName].filter(Boolean).join(" "); - const displayName = - fullName || - (username ? `@${username.replace(/^@/, "")}` : null) || - title || - `${TELEGRAM_PLACEHOLDER_PREFIX}${externalContactId}`; - - return { - displayName, - avatarUrl: asString(normalized.contactAvatarUrl), - }; -} - -async function maybeHydrateContact(contactId: string, profile: ContactProfile, externalContactId: string) { - const current = await prisma.contact.findUnique({ - where: { id: contactId }, - select: { name: true, avatarUrl: true }, - }); - if (!current) return; - - const updates: Prisma.ContactUpdateInput = {}; - const currentName = asString(current.name); - const nextName = asString(profile.displayName); - - if (nextName && (!currentName || currentName.startsWith(TELEGRAM_PLACEHOLDER_PREFIX)) && currentName !== nextName) { - updates.name = nextName; - } - - const currentAvatar = asString(current.avatarUrl); - const resolvedAvatarUrl = profile.avatarUrl ?? (currentAvatar ? null : await fetchTelegramAvatarUrl(externalContactId)); - if (resolvedAvatarUrl && resolvedAvatarUrl !== currentAvatar) { - updates.avatarUrl = resolvedAvatarUrl; - } - - if (Object.keys(updates).length === 0) return; - await prisma.contact.update({ - where: { id: contactId }, - data: updates, - }); -} - -async function resolveTeamId(env: OmniInboundEnvelopeV1) { - const n = env.payloadNormalized ?? ({} as OmniInboundEnvelopeV1["payloadNormalized"]); - const bcId = String(n.businessConnectionId ?? "").trim(); - if (bcId) { - const linked = await prisma.telegramBusinessConnection.findFirst({ - where: { businessConnectionId: bcId }, - orderBy: { updatedAt: "desc" }, - select: { teamId: true }, - }); - if (linked?.teamId) return linked.teamId; - } - - const externalContactId = String(n.contactExternalId ?? n.threadExternalId ?? "").trim(); - if (externalContactId) { - const pseudo = `link:${externalContactId}`; - const linked = await prisma.telegramBusinessConnection.findFirst({ - where: { businessConnectionId: pseudo }, - orderBy: { updatedAt: "desc" }, - select: { teamId: true }, - }); - if (linked?.teamId) return linked.teamId; - } - - const fallbackTeamId = String(process.env.DEFAULT_TEAM_ID || "").trim(); - if (fallbackTeamId) return fallbackTeamId; - - const demo = await prisma.team.findFirst({ - where: { id: "demo-team" }, - select: { id: true }, - }); - return demo?.id ?? null; -} - -async function resolveContact(input: { - teamId: string; - externalContactId: string; - profile: ContactProfile; -}) { - const existingIdentity = await prisma.omniContactIdentity.findFirst({ - where: { - teamId: input.teamId, - channel: "TELEGRAM", - externalId: input.externalContactId, - }, - select: { contactId: true }, - }); - if (existingIdentity?.contactId) { - await maybeHydrateContact(existingIdentity.contactId, input.profile, input.externalContactId); - return existingIdentity.contactId; - } - - const avatarUrl = input.profile.avatarUrl ?? (await fetchTelegramAvatarUrl(input.externalContactId)); - - const contact = await prisma.contact.create({ - data: { - teamId: input.teamId, - name: input.profile.displayName, - avatarUrl, - }, - select: { id: true }, - }); - - try { - await prisma.omniContactIdentity.create({ - data: { - teamId: input.teamId, - contactId: contact.id, - channel: "TELEGRAM", - externalId: input.externalContactId, - }, - }); - } catch (error) { - if (!isUniqueConstraintError(error)) throw error; - - const concurrentIdentity = await prisma.omniContactIdentity.findFirst({ - where: { - teamId: input.teamId, - channel: "TELEGRAM", - externalId: input.externalContactId, - }, - select: { contactId: true }, - }); - if (!concurrentIdentity?.contactId) throw error; - - await prisma.contact.delete({ where: { id: contact.id } }).catch(() => undefined); - await maybeHydrateContact(concurrentIdentity.contactId, input.profile, input.externalContactId); - return concurrentIdentity.contactId; - } - - return contact.id; -} - -async function upsertThread(input: { - teamId: string; - contactId: string; - externalChatId: string; - businessConnectionId: string | null; - title: string | null; -}) { - const existing = await prisma.omniThread.findFirst({ - where: { - teamId: input.teamId, - channel: "TELEGRAM", - externalChatId: input.externalChatId, - businessConnectionId: input.businessConnectionId, - }, - select: { id: true, title: true }, - }); - - if (existing) { - const data: Prisma.OmniThreadUpdateInput = { - contact: { connect: { id: input.contactId } }, - }; - if (input.title && !existing.title) { - data.title = input.title; - } - - await prisma.omniThread.update({ - where: { id: existing.id }, - data, - }); - return existing; - } - - try { - return await prisma.omniThread.create({ - data: { - teamId: input.teamId, - contactId: input.contactId, - channel: "TELEGRAM", - externalChatId: input.externalChatId, - businessConnectionId: input.businessConnectionId, - title: input.title, - }, - select: { id: true }, - }); - } catch (error) { - if (!isUniqueConstraintError(error)) throw error; - - const concurrentThread = await prisma.omniThread.findFirst({ - where: { - teamId: input.teamId, - channel: "TELEGRAM", - externalChatId: input.externalChatId, - businessConnectionId: input.businessConnectionId, - }, - select: { id: true }, - }); - if (!concurrentThread) throw error; - - await prisma.omniThread.update({ - where: { id: concurrentThread.id }, - data: { contact: { connect: { id: input.contactId } } }, - }); - return concurrentThread; - } -} - -async function upsertContactInbox(input: { - teamId: string; - contactId: string; - channel: "TELEGRAM"; - sourceExternalId: string; - title: string | null; -}) { - return prisma.contactInbox.upsert({ - where: { - teamId_channel_sourceExternalId: { - teamId: input.teamId, - channel: input.channel, - sourceExternalId: input.sourceExternalId, - }, - }, - create: { - teamId: input.teamId, - contactId: input.contactId, - channel: input.channel, - sourceExternalId: input.sourceExternalId, - title: input.title, - }, - update: { - contactId: input.contactId, - ...(input.title ? { title: input.title } : {}), - }, - select: { id: true }, - }); -} - -async function handleReadBusinessMessage(env: OmniInboundEnvelopeV1) { - const teamId = await resolveTeamId(env); - if (!teamId) return; - - const n = env.payloadNormalized ?? ({} as OmniInboundEnvelopeV1["payloadNormalized"]); - const externalChatId = String(n.threadExternalId ?? n.contactExternalId ?? "").trim(); - if (!externalChatId) return; - - const thread = await prisma.omniThread.findFirst({ - where: { teamId, channel: "TELEGRAM", externalChatId }, - select: { contactId: true }, - }); - if (!thread) return; - - const teamUsers = await prisma.teamMember.findMany({ - where: { teamId }, - select: { userId: true }, - }); - const now = new Date(); - // ContactThreadRead is not in omni_chat's Prisma schema, use raw upsert - await Promise.all( - teamUsers.map((u) => - prisma.$executeRaw` - INSERT INTO "ContactThreadRead" ("id", "teamId", "userId", "contactId", "readAt") - VALUES (gen_random_uuid(), ${teamId}, ${u.userId}, ${thread.contactId}, ${now}) - ON CONFLICT ("userId", "contactId") DO UPDATE SET "readAt" = ${now} - `, - ), - ); - console.log(`[omni_chat] read_business_message: marked contact ${thread.contactId} as read for ${teamUsers.length} users`); -} - -async function ingestInbound(env: OmniInboundEnvelopeV1) { - if (env.channel !== "TELEGRAM") return; - - if (env.eventType === "read_business_message") { - await handleReadBusinessMessage(env); - return; - } - - const teamId = await resolveTeamId(env); - if (!teamId) { - console.warn("[omni_chat] skip inbound: team not resolved", env.providerEventId); - return; - } - - const n = env.payloadNormalized ?? ({} as OmniInboundEnvelopeV1["payloadNormalized"]); - const externalContactId = String(n.contactExternalId ?? n.threadExternalId ?? "").trim(); - const externalChatId = String(n.threadExternalId ?? n.contactExternalId ?? "").trim(); - - if (!externalContactId || !externalChatId) { - console.warn("[omni_chat] skip inbound: missing contact/chat ids", env.providerEventId); - return; - } - - const businessConnectionId = String(n.businessConnectionId ?? "").trim() || null; - const media = parseTelegramInboundMedia(n); - const text = normalizeText(asString(n.text) ?? fallbackTextFromMedia(media)); - const isAudioLike = Boolean(media.fileId) && (media.kind === "voice" || media.kind === "audio" || media.kind === "video_note"); - const contactMessageKind: "MESSAGE" | "CALL" = isAudioLike ? "CALL" : "MESSAGE"; - const contactMessageAudioUrl = isAudioLike ? `${TELEGRAM_AUDIO_FILE_MARKER}${media.fileId}` : null; - const waveformPeaks = isAudioLike ? await resolveInboundWaveform(media, text) : null; - const occurredAt = parseOccurredAt(env.occurredAt); - const direction = safeDirection(env.direction); - const contactProfile = buildContactProfile(n, externalContactId); - - const contactId = await resolveContact({ - teamId, - externalContactId, - profile: contactProfile, - }); - const thread = await upsertThread({ - teamId, - contactId, - externalChatId, - businessConnectionId, - title: asString(n.chatTitle), - }); - const inbox = await upsertContactInbox({ - teamId, - contactId, - channel: "TELEGRAM", - sourceExternalId: externalChatId, - title: asString(n.chatTitle), - }); - const rawEnvelope = { - version: env.version, - source: "omni_chat.receiver", - provider: env.provider, - channel: env.channel, - direction, - providerEventId: env.providerEventId, - receivedAt: env.receivedAt, - occurredAt: occurredAt.toISOString(), - normalized: { - text, - threadExternalId: externalChatId, - contactExternalId: externalContactId, - businessConnectionId, - mediaKind: media.kind, - mediaFileId: media.fileId, - mediaDurationSec: media.durationSec, - mediaLabel: media.label, - }, - payloadNormalized: n, - payloadRaw: (env.payloadRaw ?? null) as Prisma.InputJsonValue, - } as Prisma.InputJsonValue; - - if (env.providerMessageId) { - await prisma.omniMessage.upsert({ - where: { - threadId_providerMessageId: { - threadId: thread.id, - providerMessageId: env.providerMessageId, - }, - }, - create: { - teamId, - contactId, - threadId: thread.id, - direction, - channel: "TELEGRAM", - status: "DELIVERED", - text, - providerMessageId: env.providerMessageId, - providerUpdateId: String((n.updateId as string | null | undefined) ?? env.providerEventId), - rawJson: rawEnvelope, - occurredAt, - }, - update: { - text, - providerUpdateId: String((n.updateId as string | null | undefined) ?? env.providerEventId), - rawJson: rawEnvelope, - occurredAt, - }, - }); - } else { - await prisma.omniMessage.create({ - data: { - teamId, - contactId, - threadId: thread.id, - direction, - channel: "TELEGRAM", - status: "DELIVERED", - text, - providerMessageId: null, - providerUpdateId: String((n.updateId as string | null | undefined) ?? env.providerEventId), - rawJson: rawEnvelope, - occurredAt, - }, - }); - } - - await prisma.contactMessage.create({ - data: { - contactId, - contactInboxId: inbox.id, - kind: contactMessageKind, - direction, - channel: "TELEGRAM", - content: text, - audioUrl: contactMessageAudioUrl, - durationSec: media.durationSec, - ...(waveformPeaks ? { waveformJson: waveformPeaks as Prisma.InputJsonValue } : {}), - occurredAt, - }, - }); -} - -let workerInstance: Worker | null = null; - -export function startReceiverWorker() { - if (workerInstance) return workerInstance; - - const worker = new Worker( - RECEIVER_FLOW_QUEUE_NAME, - async (job) => { - await ingestInbound(job.data); - }, - { - connection: redisConnectionFromEnv(), - concurrency: Number(process.env.OMNI_CHAT_WORKER_CONCURRENCY || 4), - }, - ); - - worker.on("failed", (job: Job | undefined, err: Error) => { - console.error(`[omni_chat] receiver job failed id=${job?.id || "unknown"}: ${err?.message || err}`); - }); - - workerInstance = worker; - return worker; -} - -export async function closeReceiverWorker() { - if (!workerInstance) return; - await workerInstance.close(); - workerInstance = null; -} - -export function receiverQueue() { - return new Queue(RECEIVER_FLOW_QUEUE_NAME, { - connection: redisConnectionFromEnv(), - }); -} diff --git a/omni_inbound/README.md b/omni_inbound/README.md deleted file mode 100644 index f8482aa..0000000 --- a/omni_inbound/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# omni_inbound - -Отдельный сервис приема входящих webhook-событий каналов (первый канал: Telegram Business). - -## Задача сервиса - -- принимать webhook; -- валидировать секрет; -- нормализовать событие в универсальный envelope; -- делать durable enqueue в BullMQ (`receiver.flow`); -- возвращать `200` только после успешного enqueue. - -Сервис **не** содержит бизнес-логику CRM и не вызывает provider API для исходящих сообщений. - -## API - -### `GET /health` -Проверка живости сервиса. - -### `POST /webhooks/telegram/business` -Прием Telegram Business webhook. - -При активном `TELEGRAM_WEBHOOK_SECRET` ожидается заголовок: - -- `x-telegram-bot-api-secret-token: ` - -## Переменные окружения - -- `PORT` (default: `8080`) -- `REDIS_URL` (default: `redis://localhost:6379`) -- `RECEIVER_FLOW_QUEUE_NAME` (default: `receiver.flow`) -- `INBOUND_QUEUE_NAME` (legacy alias, optional) -- `TELEGRAM_WEBHOOK_SECRET` (optional, но обязателен для production) -- `TELEGRAM_CONNECT_WEBHOOK_FORWARD_URL` (optional; URL CRM endpoint для линковки Telegram Business) -- `MAX_BODY_SIZE_BYTES` (default: `1048576`) - -## Запуск - -```bash -npm ci -npm run start -``` - -## Надежность - -- Идемпотентность: `jobId` строится из `idempotencyKey` (SHA-256). -- Дубликаты webhook безопасны и не приводят к повторной постановке события. -- При ошибке enqueue сервис возвращает `503`, чтобы провайдер повторил доставку. diff --git a/omni_inbound/package-lock.json b/omni_inbound/package-lock.json deleted file mode 100644 index db56168..0000000 --- a/omni_inbound/package-lock.json +++ /dev/null @@ -1,908 +0,0 @@ -{ - "name": "crm-omni-inbound", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "crm-omni-inbound", - "dependencies": { - "bullmq": "^5.58.2" - }, - "devDependencies": { - "@types/node": "^22.13.9", - "tsx": "^4.20.5", - "typescript": "^5.9.2" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", - "license": "MIT" - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/bullmq": { - "version": "5.69.4", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.69.4.tgz", - "integrity": "sha512-Lp7ymp875I/rtjMm6oxzQ3PrvDDHkgge0oaAznmZsKtGyglfdrg9zbidPSszTXgWFkS2rCgMcTRNJfM3uUMOjQ==", - "license": "MIT", - "dependencies": { - "cron-parser": "4.9.0", - "ioredis": "5.9.2", - "msgpackr": "1.11.5", - "node-abort-controller": "3.1.1", - "semver": "7.7.4", - "tslib": "2.8.1", - "uuid": "11.1.0" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/ioredis": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", - "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.5.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - } - } -} diff --git a/omni_inbound/src/queue.ts b/omni_inbound/src/queue.ts deleted file mode 100644 index 9635b48..0000000 --- a/omni_inbound/src/queue.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createHash } from "node:crypto"; -import { Queue, type ConnectionOptions } from "bullmq"; -import type { OmniInboundEnvelopeV1 } from "./types"; - -export const RECEIVER_FLOW_QUEUE_NAME = ( - process.env.RECEIVER_FLOW_QUEUE_NAME || - process.env.INBOUND_QUEUE_NAME || - "receiver.flow" -).trim(); - -let queueInstance: Queue | null = null; - -function redisConnectionFromEnv(): ConnectionOptions { - const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim(); - const parsed = new URL(raw); - - return { - host: parsed.hostname, - port: parsed.port ? Number(parsed.port) : 6379, - username: parsed.username ? decodeURIComponent(parsed.username) : undefined, - password: parsed.password ? decodeURIComponent(parsed.password) : undefined, - db: parsed.pathname && parsed.pathname !== "/" ? Number(parsed.pathname.slice(1)) : undefined, - maxRetriesPerRequest: null, - }; -} - -function toJobId(idempotencyKey: string) { - const hash = createHash("sha256").update(idempotencyKey).digest("hex"); - return `inbound-${hash}`; -} - -export function inboundQueue() { - if (queueInstance) return queueInstance; - - queueInstance = new Queue(RECEIVER_FLOW_QUEUE_NAME, { - connection: redisConnectionFromEnv(), - defaultJobOptions: { - removeOnComplete: { count: 10000 }, - removeOnFail: { count: 20000 }, - attempts: 8, - backoff: { type: "exponential", delay: 1000 }, - }, - }); - - return queueInstance; -} - -export async function enqueueInboundEvent(envelope: OmniInboundEnvelopeV1) { - const q = inboundQueue(); - const jobId = toJobId(envelope.idempotencyKey); - - return q.add("ingest", envelope, { - jobId, - }); -} - -export function isDuplicateJobError(error: unknown) { - if (!error || typeof error !== "object") return false; - const message = String((error as { message?: string }).message || "").toLowerCase(); - return message.includes("job") && message.includes("exists"); -} - -export async function closeInboundQueue() { - if (!queueInstance) return; - await queueInstance.close(); - queueInstance = null; -} diff --git a/omni_inbound/src/server.ts b/omni_inbound/src/server.ts deleted file mode 100644 index 55b3b8f..0000000 --- a/omni_inbound/src/server.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; -import { RECEIVER_FLOW_QUEUE_NAME, enqueueInboundEvent, isDuplicateJobError } from "./queue"; -import { parseTelegramBusinessUpdate } from "./telegram"; - -const MAX_BODY_SIZE_BYTES = Number(process.env.MAX_BODY_SIZE_BYTES || 1024 * 1024); - -function writeJson(res: ServerResponse, statusCode: number, body: unknown) { - const payload = JSON.stringify(body); - res.statusCode = statusCode; - res.setHeader("content-type", "application/json; charset=utf-8"); - res.end(payload); -} - -async function readJsonBody(req: IncomingMessage): Promise { - const chunks: Buffer[] = []; - let total = 0; - - for await (const chunk of req) { - const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - total += buf.length; - if (total > MAX_BODY_SIZE_BYTES) { - throw new Error(`payload_too_large:${MAX_BODY_SIZE_BYTES}`); - } - chunks.push(buf); - } - - const raw = Buffer.concat(chunks).toString("utf8"); - if (!raw) return {}; - return JSON.parse(raw); -} - -function validateTelegramSecret(req: IncomingMessage): boolean { - const expected = (process.env.TELEGRAM_WEBHOOK_SECRET || "").trim(); - if (!expected) return true; - - const incoming = String(req.headers["x-telegram-bot-api-secret-token"] || "").trim(); - return incoming !== "" && incoming === expected; -} - -async function forwardTelegramConnectWebhook(rawBody: unknown) { - const url = (process.env.TELEGRAM_CONNECT_WEBHOOK_FORWARD_URL || "").trim(); - if (!url) return; - - const headers: Record = { - "content-type": "application/json", - }; - const secret = (process.env.TELEGRAM_WEBHOOK_SECRET || "").trim(); - if (secret) { - headers["x-telegram-bot-api-secret-token"] = secret; - } - - try { - const res = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(rawBody ?? {}), - }); - - if (!res.ok) { - const text = await res.text().catch(() => ""); - console.warn(`[omni_inbound] telegram connect forward failed: ${res.status} ${text.slice(0, 300)}`); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`[omni_inbound] telegram connect forward error: ${message}`); - } -} - -export function startServer() { - const port = Number(process.env.PORT || 8080); - - const server = createServer(async (req, res) => { - if (!req.url || !req.method) { - writeJson(res, 404, { ok: false, error: "not_found" }); - return; - } - - if (req.url === "/health" && req.method === "GET") { - writeJson(res, 200, { - ok: true, - service: "omni_inbound", - queue: RECEIVER_FLOW_QUEUE_NAME, - now: new Date().toISOString(), - }); - return; - } - - if (req.url === "/webhooks/telegram/business" && req.method === "POST") { - if (!validateTelegramSecret(req)) { - writeJson(res, 401, { ok: false, error: "invalid_webhook_secret" }); - return; - } - - let body: unknown = {}; - let envelope: ReturnType | null = null; - - try { - body = await readJsonBody(req); - envelope = parseTelegramBusinessUpdate(body); - - await enqueueInboundEvent(envelope); - - void forwardTelegramConnectWebhook(body); - - writeJson(res, 200, { - ok: true, - queued: true, - duplicate: false, - providerEventId: envelope.providerEventId, - idempotencyKey: envelope.idempotencyKey, - }); - } catch (error) { - if (isDuplicateJobError(error)) { - void forwardTelegramConnectWebhook(body); - - writeJson(res, 200, { - ok: true, - queued: false, - duplicate: true, - }); - return; - } - - const message = error instanceof Error ? error.message : String(error); - const statusCode = message.startsWith("payload_too_large:") ? 413 : 503; - writeJson(res, statusCode, { - ok: false, - error: "receiver_enqueue_failed", - message, - }); - } - - return; - } - - writeJson(res, 404, { ok: false, error: "not_found" }); - }); - - server.listen(port, "0.0.0.0", () => { - console.log(`[omni_inbound] listening on :${port}`); - }); - - return server; -} diff --git a/omni_outbound/Dockerfile b/omni_outbound/Dockerfile deleted file mode 100644 index aefd117..0000000 --- a/omni_outbound/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:22-bookworm-slim - -WORKDIR /app/delivery - -RUN apt-get update -y \ - && apt-get install -y --no-install-recommends openssl ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -COPY package*.json ./ -RUN npm ci --legacy-peer-deps - -COPY prisma ./prisma -RUN npx prisma generate - -COPY src ./src -COPY tsconfig.json ./tsconfig.json - -ENV NODE_ENV=production - -CMD ["npm", "run", "start"] diff --git a/omni_outbound/README.md b/omni_outbound/README.md deleted file mode 100644 index c7ae740..0000000 --- a/omni_outbound/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# omni_outbound - -Изолированный сервис исходящей доставки. - -## Назначение - -- потребляет задачи из `sender.flow`; -- выполняет отправку в провайдеров; -- применяет retry/backoff и финальный fail-status. - -## Переменные окружения - -- `REDIS_URL` -- `DATABASE_URL` -- `SENDER_FLOW_QUEUE_NAME` (default: `sender.flow`) -- `OUTBOUND_DELIVERY_QUEUE_NAME` (legacy alias, optional) - -## Prisma policy - -- Источник схемы: `frontend/prisma/schema.prisma`. -- Локальная копия в `omni_outbound/prisma/schema.prisma` обновляется только через `scripts/prisma-sync.sh`. -- Миграции/`db push` выполняются только в `frontend`. diff --git a/omni_outbound/package-lock.json b/omni_outbound/package-lock.json deleted file mode 100644 index a4ddda4..0000000 --- a/omni_outbound/package-lock.json +++ /dev/null @@ -1,1358 +0,0 @@ -{ - "name": "crm-omni-outbound", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "crm-omni-outbound", - "dependencies": { - "@prisma/client": "^6.16.1", - "bullmq": "^5.58.2", - "ioredis": "^5.7.0" - }, - "devDependencies": { - "@types/node": "^22.13.9", - "prisma": "^6.16.1", - "tsx": "^4.20.5", - "typescript": "^5.9.2" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", - "license": "MIT" - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@prisma/client": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", - "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", - "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "c12": "3.1.0", - "deepmerge-ts": "7.1.5", - "effect": "3.18.4", - "empathic": "2.0.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", - "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", - "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.19.2", - "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "@prisma/fetch-engine": "6.19.2", - "@prisma/get-platform": "6.19.2" - } - }, - "node_modules/@prisma/engines-version": { - "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", - "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", - "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.19.2", - "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", - "@prisma/get-platform": "6.19.2" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", - "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.19.2" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/bullmq": { - "version": "5.69.3", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.69.3.tgz", - "integrity": "sha512-P9uLsR7fDvejH/1m6uur6j7U9mqY6nNt+XvhlhStOUe7jdwbZoP/c2oWNtE+8ljOlubw4pRUKymtRqkyvloc4A==", - "license": "MIT", - "dependencies": { - "cron-parser": "4.9.0", - "ioredis": "5.9.2", - "msgpackr": "1.11.5", - "node-abort-controller": "3.1.1", - "semver": "7.7.4", - "tslib": "2.8.1", - "uuid": "11.1.0" - } - }, - "node_modules/bullmq/node_modules/ioredis": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", - "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.5.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true, - "license": "MIT" - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/ioredis": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", - "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.5.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/nypm": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", - "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "citty": "^0.2.0", - "pathe": "^2.0.3", - "tinyexec": "^1.0.2" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", - "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/prisma": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", - "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/config": "6.19.2", - "@prisma/engines": "6.19.2" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - } - } -} diff --git a/omni_outbound/prisma/schema.prisma b/omni_outbound/prisma/schema.prisma deleted file mode 100644 index cdb7221..0000000 --- a/omni_outbound/prisma/schema.prisma +++ /dev/null @@ -1,437 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -enum TeamRole { - OWNER - MEMBER -} - -enum MessageDirection { - IN - OUT -} - -enum MessageChannel { - TELEGRAM - WHATSAPP - INSTAGRAM - PHONE - EMAIL - INTERNAL -} - -enum ContactMessageKind { - MESSAGE - CALL -} - -enum ChatRole { - USER - ASSISTANT - SYSTEM -} - -enum OmniMessageStatus { - PENDING - SENT - FAILED - DELIVERED - READ -} - -enum FeedCardDecision { - PENDING - ACCEPTED - REJECTED -} - -enum WorkspaceDocumentType { - Regulation - Playbook - Policy - Template -} - -model Team { - id String @id @default(cuid()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - members TeamMember[] - contacts Contact[] - calendarEvents CalendarEvent[] - deals Deal[] - aiConversations AiConversation[] - aiMessages AiMessage[] - - omniThreads OmniThread[] - omniMessages OmniMessage[] - omniIdentities OmniContactIdentity[] - telegramBusinessConnections TelegramBusinessConnection[] - - feedCards FeedCard[] - contactPins ContactPin[] - documents WorkspaceDocument[] - contactInboxes ContactInbox[] - contactInboxPreferences ContactInboxPreference[] -} - -model User { - id String @id @default(cuid()) - phone String @unique - passwordHash String - email String? @unique - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - memberships TeamMember[] - aiConversations AiConversation[] @relation("ConversationCreator") - aiMessages AiMessage[] @relation("ChatAuthor") - contactInboxPreferences ContactInboxPreference[] -} - -model TeamMember { - id String @id @default(cuid()) - teamId String - userId String - role TeamRole @default(MEMBER) - createdAt DateTime @default(now()) - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([teamId, userId]) - @@index([userId]) -} - -model Contact { - id String @id @default(cuid()) - teamId String - name String - avatarUrl String? - email String? - phone String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - note ContactNote? - messages ContactMessage[] - events CalendarEvent[] - deals Deal[] - feedCards FeedCard[] - pins ContactPin[] - - omniThreads OmniThread[] - omniMessages OmniMessage[] - omniIdentities OmniContactIdentity[] - contactInboxes ContactInbox[] - - @@index([teamId, updatedAt]) -} - -model ContactNote { - id String @id @default(cuid()) - contactId String @unique - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) -} - -model ContactMessage { - id String @id @default(cuid()) - contactId String - contactInboxId String? - kind ContactMessageKind @default(MESSAGE) - direction MessageDirection - channel MessageChannel - content String - audioUrl String? - durationSec Int? - waveformJson Json? - transcriptJson Json? - occurredAt DateTime @default(now()) - createdAt DateTime @default(now()) - - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - contactInbox ContactInbox? @relation(fields: [contactInboxId], references: [id], onDelete: SetNull) - - @@index([contactId, occurredAt]) - @@index([contactInboxId, occurredAt]) -} - -model ContactInbox { - id String @id @default(cuid()) - teamId String - contactId String - channel MessageChannel - sourceExternalId String - title String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - messages ContactMessage[] - preferences ContactInboxPreference[] - - @@unique([teamId, channel, sourceExternalId]) - @@index([contactId, updatedAt]) - @@index([teamId, updatedAt]) -} - -model ContactInboxPreference { - id String @id @default(cuid()) - teamId String - userId String - contactInboxId String - isHidden Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - contactInbox ContactInbox @relation(fields: [contactInboxId], references: [id], onDelete: Cascade) - - @@unique([userId, contactInboxId]) - @@index([teamId, userId, isHidden]) -} - -model OmniContactIdentity { - id String @id @default(cuid()) - teamId String - contactId String - channel MessageChannel - externalId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - - @@unique([teamId, channel, externalId]) - @@index([contactId]) - @@index([teamId, updatedAt]) -} - -model OmniThread { - id String @id @default(cuid()) - teamId String - contactId String - channel MessageChannel - externalChatId String - businessConnectionId String? - title String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - messages OmniMessage[] - - @@unique([teamId, channel, externalChatId, businessConnectionId]) - @@index([teamId, updatedAt]) - @@index([contactId, updatedAt]) -} - -model OmniMessage { - id String @id @default(cuid()) - teamId String - contactId String - threadId String - direction MessageDirection - channel MessageChannel - status OmniMessageStatus @default(PENDING) - text String - providerMessageId String? - providerUpdateId String? - rawJson Json? - occurredAt DateTime @default(now()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - thread OmniThread @relation(fields: [threadId], references: [id], onDelete: Cascade) - - @@unique([threadId, providerMessageId]) - @@index([teamId, occurredAt]) - @@index([threadId, occurredAt]) -} - -model TelegramBusinessConnection { - id String @id @default(cuid()) - teamId String - businessConnectionId String - isEnabled Boolean? - canReply Boolean? - rawJson Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - - @@unique([teamId, businessConnectionId]) - @@index([teamId, updatedAt]) -} - -model CalendarEvent { - id String @id @default(cuid()) - teamId String - contactId String? - title String - startsAt DateTime - endsAt DateTime? - note String? - isArchived Boolean @default(false) - archiveNote String? - archivedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) - - @@index([startsAt]) - @@index([contactId, startsAt]) - @@index([teamId, startsAt]) -} - -model Deal { - id String @id @default(cuid()) - teamId String - contactId String - title String - stage String - amount Int? - paidAmount Int? - nextStep String? - summary String? - currentStepId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - steps DealStep[] - - @@index([teamId, updatedAt]) - @@index([contactId, updatedAt]) - @@index([currentStepId]) -} - -model DealStep { - id String @id @default(cuid()) - dealId String - title String - description String? - status String @default("todo") - dueAt DateTime? - order Int @default(0) - completedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade) - - @@index([dealId, order]) - @@index([status, dueAt]) -} - -model AiConversation { - id String @id @default(cuid()) - teamId String - createdByUserId String - title String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - createdByUser User @relation("ConversationCreator", fields: [createdByUserId], references: [id], onDelete: Cascade) - messages AiMessage[] - - @@index([teamId, updatedAt]) - @@index([createdByUserId]) - @@map("ChatConversation") -} - -model AiMessage { - id String @id @default(cuid()) - teamId String - conversationId String - authorUserId String? - role ChatRole - text String - planJson Json? - createdAt DateTime @default(now()) - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - conversation AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) - authorUser User? @relation("ChatAuthor", fields: [authorUserId], references: [id], onDelete: SetNull) - - @@index([createdAt]) - @@index([teamId, createdAt]) - @@index([conversationId, createdAt]) - @@map("ChatMessage") -} - -model FeedCard { - id String @id @default(cuid()) - teamId String - contactId String? - happenedAt DateTime - text String - proposalJson Json - decision FeedCardDecision @default(PENDING) - decisionNote String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) - - @@index([teamId, happenedAt]) - @@index([contactId, happenedAt]) -} - -model ContactPin { - id String @id @default(cuid()) - teamId String - contactId String - text String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - - @@index([teamId, updatedAt]) - @@index([contactId, updatedAt]) -} - -model WorkspaceDocument { - id String @id @default(cuid()) - teamId String - title String - type WorkspaceDocumentType - owner String - scope String - summary String - body String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - - @@index([teamId, updatedAt]) -} diff --git a/omni_outbound/src/queues/outboundDelivery.ts b/omni_outbound/src/queues/outboundDelivery.ts deleted file mode 100644 index 8b7702a..0000000 --- a/omni_outbound/src/queues/outboundDelivery.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { Queue, Worker, type Job, type JobsOptions, type ConnectionOptions } from "bullmq"; -import { Prisma } from "@prisma/client"; -import { prisma } from "../utils/prisma"; - -export const OUTBOUND_DELIVERY_QUEUE_NAME = ( - process.env.SENDER_FLOW_QUEUE_NAME || - process.env.OUTBOUND_DELIVERY_QUEUE_NAME || - "sender.flow" -).trim(); - -export type OutboundDeliveryJob = { - omniMessageId: string; - endpoint: string; - method?: "POST" | "PUT" | "PATCH"; - headers?: Record; - payload: unknown; - channel?: string; - provider?: string; -}; - -function redisConnectionFromEnv(): ConnectionOptions { - const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim(); - const parsed = new URL(raw); - return { - host: parsed.hostname, - port: parsed.port ? Number(parsed.port) : 6379, - username: parsed.username ? decodeURIComponent(parsed.username) : undefined, - password: parsed.password ? decodeURIComponent(parsed.password) : undefined, - db: parsed.pathname && parsed.pathname !== "/" ? Number(parsed.pathname.slice(1)) : undefined, - maxRetriesPerRequest: null, - }; -} - -function ensureHttpUrl(value: string) { - const raw = (value ?? "").trim(); - if (!raw) throw new Error("endpoint is required"); - const parsed = new URL(raw); - if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { - throw new Error(`Unsupported endpoint protocol: ${parsed.protocol}`); - } - return parsed.toString(); -} - -function compactError(error: unknown) { - if (!error) return "unknown_error"; - if (typeof error === "string") return error; - const anyErr = error as any; - return String(anyErr?.message ?? anyErr); -} - -function extractProviderMessageId(body: unknown): string | null { - const obj = body as any; - if (!obj || typeof obj !== "object") return null; - const candidate = - obj?.message_id ?? - obj?.messageId ?? - obj?.id ?? - obj?.result?.message_id ?? - obj?.result?.id ?? - null; - if (candidate == null) return null; - return String(candidate); -} - -function asObject(value: unknown): Record { - if (!value || typeof value !== "object" || Array.isArray(value)) return {}; - return value as Record; -} - -export function outboundDeliveryQueue() { - return new Queue(OUTBOUND_DELIVERY_QUEUE_NAME, { - connection: redisConnectionFromEnv(), - defaultJobOptions: { - removeOnComplete: { count: 1000 }, - removeOnFail: { count: 5000 }, - }, - }); -} - -export async function enqueueOutboundDelivery(input: OutboundDeliveryJob, opts?: JobsOptions) { - const endpoint = ensureHttpUrl(input.endpoint); - const q = outboundDeliveryQueue(); - - const payload = (input.payload ?? null) as Prisma.InputJsonValue; - const existing = await prisma.omniMessage.findUnique({ - where: { id: input.omniMessageId }, - select: { rawJson: true }, - }); - const raw = asObject(existing?.rawJson); - const rawQueue = asObject(raw.queue); - const rawDeliveryRequest = asObject(raw.deliveryRequest); - await prisma.omniMessage.update({ - where: { id: input.omniMessageId }, - data: { - status: "PENDING", - rawJson: { - ...raw, - queue: { - ...rawQueue, - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - enqueuedAt: new Date().toISOString(), - }, - deliveryRequest: { - ...rawDeliveryRequest, - endpoint, - method: input.method ?? "POST", - channel: input.channel ?? null, - provider: input.provider ?? null, - payload, - }, - }, - }, - }); - - return q.add("deliver", { ...input, endpoint }, { - jobId: `omni-${input.omniMessageId}`, - attempts: 12, - backoff: { type: "exponential", delay: 1000 }, - ...opts, - }); -} - -export function startOutboundDeliveryWorker() { - return new Worker( - OUTBOUND_DELIVERY_QUEUE_NAME, - async (job: Job) => { - const msg = await prisma.omniMessage.findUnique({ - where: { id: job.data.omniMessageId }, - include: { thread: true }, - }); - if (!msg) return; - - if ((msg.status === "SENT" || msg.status === "DELIVERED" || msg.status === "READ") && msg.providerMessageId) { - return; - } - - const endpoint = ensureHttpUrl(job.data.endpoint); - const method = job.data.method ?? "POST"; - const headers: Record = { - "content-type": "application/json", - ...(job.data.headers ?? {}), - }; - - const requestPayload = (job.data.payload ?? null) as Prisma.InputJsonValue; - const requestStartedAt = new Date().toISOString(); - try { - const response = await fetch(endpoint, { - method, - headers, - body: JSON.stringify(requestPayload ?? {}), - }); - - const text = await response.text(); - const responseBody = (() => { - try { - return JSON.parse(text); - } catch { - return text; - } - })(); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody)}`); - } - - const providerMessageId = extractProviderMessageId(responseBody); - const raw = asObject(msg.rawJson); - const rawQueue = asObject(raw.queue); - const rawDeliveryRequest = asObject(raw.deliveryRequest); - await prisma.omniMessage.update({ - where: { id: msg.id }, - data: { - status: "SENT", - providerMessageId, - rawJson: { - ...raw, - queue: { - ...rawQueue, - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - completedAt: new Date().toISOString(), - attemptsMade: job.attemptsMade + 1, - }, - deliveryRequest: { - ...rawDeliveryRequest, - endpoint, - method, - channel: job.data.channel ?? null, - provider: job.data.provider ?? null, - startedAt: requestStartedAt, - payload: requestPayload, - }, - deliveryResponse: { - status: response.status, - body: responseBody, - }, - }, - }, - }); - } catch (error) { - const isLastAttempt = - typeof job.opts.attempts === "number" && job.attemptsMade + 1 >= job.opts.attempts; - - if (isLastAttempt) { - const raw = asObject(msg.rawJson); - const rawQueue = asObject(raw.queue); - const rawDeliveryRequest = asObject(raw.deliveryRequest); - await prisma.omniMessage.update({ - where: { id: msg.id }, - data: { - status: "FAILED", - rawJson: { - ...raw, - queue: { - ...rawQueue, - queueName: OUTBOUND_DELIVERY_QUEUE_NAME, - failedAt: new Date().toISOString(), - attemptsMade: job.attemptsMade + 1, - }, - deliveryRequest: { - ...rawDeliveryRequest, - endpoint, - method, - channel: job.data.channel ?? null, - provider: job.data.provider ?? null, - startedAt: requestStartedAt, - payload: requestPayload, - }, - deliveryError: { - message: compactError(error), - }, - }, - }, - }); - } - - throw error; - } - }, - { connection: redisConnectionFromEnv() }, - ); -} diff --git a/omni_outbound/src/utils/prisma.ts b/omni_outbound/src/utils/prisma.ts deleted file mode 100644 index b46f521..0000000 --- a/omni_outbound/src/utils/prisma.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -declare global { - // eslint-disable-next-line no-var - var __prisma: PrismaClient | undefined; -} - -export const prisma = - globalThis.__prisma ?? - new PrismaClient({ - log: ["error", "warn"], - }); - -if (process.env.NODE_ENV !== "production") { - globalThis.__prisma = prisma; -} diff --git a/omni_outbound/src/utils/redis.ts b/omni_outbound/src/utils/redis.ts deleted file mode 100644 index b07d9f0..0000000 --- a/omni_outbound/src/utils/redis.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Redis, { type Redis as RedisClient } from "ioredis"; - -declare global { - // eslint-disable-next-line no-var - var __redis: RedisClient | undefined; -} - -export function getRedis() { - if (globalThis.__redis) return globalThis.__redis; - - const url = process.env.REDIS_URL || "redis://localhost:6379"; - const client = new Redis(url, { - maxRetriesPerRequest: null, - }); - - if (process.env.NODE_ENV !== "production") { - globalThis.__redis = client; - } - - return client; -} diff --git a/omni_outbound/src/worker.ts b/omni_outbound/src/worker.ts deleted file mode 100644 index 5db933b..0000000 --- a/omni_outbound/src/worker.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OUTBOUND_DELIVERY_QUEUE_NAME, startOutboundDeliveryWorker } from "./queues/outboundDelivery"; -import { prisma } from "./utils/prisma"; -import { getRedis } from "./utils/redis"; - -const worker = startOutboundDeliveryWorker(); -console.log(`[omni_outbound] started queue ${OUTBOUND_DELIVERY_QUEUE_NAME}`); - -async function shutdown(signal: string) { - console.log(`[omni_outbound] shutting down by ${signal}`); - try { - await worker.close(); - } catch { - // ignore shutdown errors - } - try { - const redis = getRedis(); - await redis.quit(); - } catch { - // ignore shutdown errors - } - try { - await prisma.$disconnect(); - } catch { - // ignore shutdown errors - } - process.exit(0); -} - -process.on("SIGINT", () => { - void shutdown("SIGINT"); -}); -process.on("SIGTERM", () => { - void shutdown("SIGTERM"); -}); diff --git a/scripts/prisma-check.sh b/scripts/prisma-check.sh index 099cca6..193042e 100755 --- a/scripts/prisma-check.sh +++ b/scripts/prisma-check.sh @@ -2,10 +2,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -CANONICAL_SCHEMA="$ROOT_DIR/frontend/prisma/schema.prisma" +CANONICAL_SCHEMA="$ROOT_DIR/Frontend/prisma/schema.prisma" TARGETS=( - "$ROOT_DIR/omni_chat/prisma/schema.prisma" - "$ROOT_DIR/omni_outbound/prisma/schema.prisma" + "$ROOT_DIR/backend/prisma/schema.prisma" ) if [[ ! -f "$CANONICAL_SCHEMA" ]]; then @@ -33,7 +32,7 @@ done # Enforce one rollout point for schema changes: # only frontend is allowed to run db push/migration commands. if rg -n "prisma (db push|migrate|migrate deploy|migrate dev)" \ - "$ROOT_DIR/omni_chat" "$ROOT_DIR/omni_outbound" \ + "$ROOT_DIR/backend" \ --glob '!**/node_modules/**' \ --glob '!**/package-lock.json' \ --glob '!**/README.md' > /tmp/prisma_non_frontend_migrations.txt; then @@ -41,7 +40,7 @@ if rg -n "prisma (db push|migrate|migrate deploy|migrate dev)" \ cat /tmp/prisma_non_frontend_migrations.txt >&2 status=1 else - echo "[prisma-check] OK: no migration/db push commands in omni services" + echo "[prisma-check] OK: no migration/db push commands in backend service" fi if [[ "$status" -ne 0 ]]; then diff --git a/scripts/prisma-sync.sh b/scripts/prisma-sync.sh index 23c880f..26e51f1 100755 --- a/scripts/prisma-sync.sh +++ b/scripts/prisma-sync.sh @@ -2,10 +2,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -CANONICAL_SCHEMA="$ROOT_DIR/frontend/prisma/schema.prisma" +CANONICAL_SCHEMA="$ROOT_DIR/Frontend/prisma/schema.prisma" TARGETS=( - "$ROOT_DIR/omni_chat/prisma/schema.prisma" - "$ROOT_DIR/omni_outbound/prisma/schema.prisma" + "$ROOT_DIR/backend/prisma/schema.prisma" ) if [[ ! -f "$CANONICAL_SCHEMA" ]]; then diff --git a/omni_inbound/Dockerfile b/telegram_backend/Dockerfile similarity index 100% rename from omni_inbound/Dockerfile rename to telegram_backend/Dockerfile diff --git a/telegram_backend/README.md b/telegram_backend/README.md new file mode 100644 index 0000000..33b262b --- /dev/null +++ b/telegram_backend/README.md @@ -0,0 +1,58 @@ +# telegram_backend + +Ingress/API адаптер Telegram Business. + +## Задача сервиса + +- принимать webhook Telegram и нормализовать payload в envelope; +- ставить задачи в Hatchet (`process-telegram-inbound`, `process-telegram-outbound`); +- предоставлять GraphQL API: + - `enqueueTelegramOutbound` (для `backend`); + - `sendTelegramMessage` (для `telegram_worker`). + +Сервис **не** содержит CRM бизнес-логику и не использует Prisma. + +## API + +### `GET /health` +Проверка живости сервиса. + +### `POST /webhooks/telegram/business` +Прием Telegram Business webhook. + +При активном `TELEGRAM_WEBHOOK_SECRET` ожидается заголовок: + +- `x-telegram-bot-api-secret-token: ` + +### `POST /graphql` + +Мутации: + +- `enqueueTelegramOutbound(input: TelegramOutboundTaskInput!): TaskEnqueueResult!` +- `sendTelegramMessage(input: TelegramSendMessageInput!): TelegramSendResult!` + +## Переменные окружения + +- `PORT` (default: `8080`) +- `MAX_BODY_SIZE_BYTES` (default: `1048576`) +- `TELEGRAM_WEBHOOK_SECRET` (optional, но обязателен для production) +- `TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET` (optional) +- `TELEGRAM_BOT_TOKEN` (required для `sendTelegramMessage`) +- `TELEGRAM_API_BASE` (default: `https://api.telegram.org`) +- `HATCHET_CLIENT_TOKEN` (required) +- `HATCHET_CLIENT_TLS_STRATEGY` (optional, например `none` для self-host без TLS) +- `HATCHET_CLIENT_HOST_PORT` (optional, например `hatchet-engine:7070`) +- `HATCHET_CLIENT_API_URL` (optional) + +## Запуск + +```bash +npm ci +npm run start +``` + +## Надежность + +- `200` на webhook возвращается только после успешного enqueue задачи в Hatchet. +- При ошибке enqueue сервис возвращает `503`, чтобы Telegram повторил доставку. +- Ретраи и оркестрация выполняются в Hatchet worker (`telegram_worker`). diff --git a/telegram_backend/package-lock.json b/telegram_backend/package-lock.json new file mode 100644 index 0000000..39af2dd --- /dev/null +++ b/telegram_backend/package-lock.json @@ -0,0 +1,1466 @@ +{ + "name": "crm-telegram-backend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crm-telegram-backend", + "dependencies": { + "@hatchet-dev/typescript-sdk": "^1.15.2", + "graphql": "^16.13.1" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^4.20.5", + "typescript": "^5.9.2" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hatchet-dev/typescript-sdk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@hatchet-dev/typescript-sdk/-/typescript-sdk-1.15.2.tgz", + "integrity": "sha512-y9PY8m7thGBoIjJdmM+0x6F8qT8u+ZdrmzOQK3jp/TpCspR8PtsKmVzLc4zQiRBtzzSw0GVJbbA9xZG6zZlz3A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.2.5", + "@types/qs": "^6.9.18", + "abort-controller-x": "^0.4.3", + "axios": "^1.13.5", + "long": "^5.3.1", + "nice-grpc": "^2.1.12", + "nice-grpc-common": "^2.0.2", + "protobufjs": "^7.4.0", + "qs": "^6.14.2", + "semver": "^7.7.1", + "yaml": "^2.7.1", + "zod": "^3.24.2", + "zod-to-json-schema": "^3.24.1" + }, + "optionalDependencies": { + "prom-client": "^15.1.3" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/abort-controller-x": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", + "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT", + "optional": true + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-grpc": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.14.tgz", + "integrity": "sha512-GK9pKNxlvnU5FAdaw7i2FFuR9CqBspcE+if2tqnKXBcE0R8525wj4BZvfcwj7FjvqbssqKxRHt2nwedalbJlww==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.14.0", + "abort-controller-x": "^0.4.0", + "nice-grpc-common": "^2.0.2" + } + }, + "node_modules/nice-grpc-common": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz", + "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", + "license": "MIT", + "dependencies": { + "ts-error": "^1.0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "optional": true, + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/ts-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz", + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "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": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/omni_inbound/package.json b/telegram_backend/package.json similarity index 71% rename from omni_inbound/package.json rename to telegram_backend/package.json index f2a4bab..8fe40c6 100644 --- a/omni_inbound/package.json +++ b/telegram_backend/package.json @@ -1,17 +1,18 @@ { - "name": "crm-omni-inbound", + "name": "crm-telegram-backend", "private": true, "type": "module", "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit" }, - "dependencies": { - "bullmq": "^5.58.2" - }, "devDependencies": { "@types/node": "^22.13.9", "tsx": "^4.20.5", "typescript": "^5.9.2" + }, + "dependencies": { + "@hatchet-dev/typescript-sdk": "^1.15.2", + "graphql": "^16.13.1" } } diff --git a/telegram_backend/src/hatchet/client.ts b/telegram_backend/src/hatchet/client.ts new file mode 100644 index 0000000..128a422 --- /dev/null +++ b/telegram_backend/src/hatchet/client.ts @@ -0,0 +1,3 @@ +import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1"; + +export const hatchet = HatchetClient.init(); diff --git a/telegram_backend/src/hatchet/tasks.ts b/telegram_backend/src/hatchet/tasks.ts new file mode 100644 index 0000000..54e3e0f --- /dev/null +++ b/telegram_backend/src/hatchet/tasks.ts @@ -0,0 +1,31 @@ +import { hatchet } from "./client"; +import type { OmniInboundEnvelopeV1 } from "../types"; + +export type TelegramOutboundTaskInput = { + omniMessageId: string; + chatId: string; + text: string; + businessConnectionId?: string | null; +}; + +const processTelegramInbound = hatchet.task({ + name: "process-telegram-inbound", +}); + +const processTelegramOutbound = hatchet.task({ + name: "process-telegram-outbound", +}); + +export async function enqueueTelegramInboundTask(input: OmniInboundEnvelopeV1) { + const run = await processTelegramInbound.runNoWait(input as any); + return { + runId: await run.runId, + }; +} + +export async function enqueueTelegramOutboundTask(input: TelegramOutboundTaskInput) { + const run = await processTelegramOutbound.runNoWait(input as any); + return { + runId: await run.runId, + }; +} diff --git a/telegram_backend/src/index.ts b/telegram_backend/src/index.ts new file mode 100644 index 0000000..6fff146 --- /dev/null +++ b/telegram_backend/src/index.ts @@ -0,0 +1,31 @@ +import { startServer } from "./server"; + +const server = startServer(); + +async function shutdown(signal: string) { + console.log(`[telegram_backend] shutting down by ${signal}`); + + try { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } catch { + // ignore shutdown errors + } + + process.exit(0); +} + +process.on("SIGINT", () => { + void shutdown("SIGINT"); +}); + +process.on("SIGTERM", () => { + void shutdown("SIGTERM"); +}); diff --git a/telegram_backend/src/server.ts b/telegram_backend/src/server.ts new file mode 100644 index 0000000..3027895 --- /dev/null +++ b/telegram_backend/src/server.ts @@ -0,0 +1,270 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { buildSchema, graphql } from "graphql"; +import { parseTelegramBusinessUpdate } from "./telegram"; +import { enqueueTelegramInboundTask, enqueueTelegramOutboundTask } from "./hatchet/tasks"; + +const PORT = Number(process.env.PORT || 8080); +const MAX_BODY_SIZE_BYTES = Number(process.env.MAX_BODY_SIZE_BYTES || 1024 * 1024); +const WEBHOOK_SECRET = String(process.env.TELEGRAM_WEBHOOK_SECRET || "").trim(); +const GRAPHQL_SHARED_SECRET = String(process.env.TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET || "").trim(); + +const schema = buildSchema(` + type Query { + health: Health! + } + + type Health { + ok: Boolean! + service: String! + now: String! + } + + input TelegramOutboundTaskInput { + omniMessageId: String! + chatId: String! + text: String! + businessConnectionId: String + } + + input TelegramSendMessageInput { + chatId: String! + text: String! + businessConnectionId: String + } + + type TaskEnqueueResult { + ok: Boolean! + message: String! + runId: String + } + + type TelegramSendResult { + ok: Boolean! + message: String! + providerMessageId: String + responseJson: String + } + + type Mutation { + enqueueTelegramOutbound(input: TelegramOutboundTaskInput!): TaskEnqueueResult! + sendTelegramMessage(input: TelegramSendMessageInput!): TelegramSendResult! + } +`); + +function asString(value: unknown) { + if (typeof value !== "string") return null; + const v = value.trim(); + return v || null; +} + +function writeJson(res: ServerResponse, statusCode: number, body: unknown) { + res.statusCode = statusCode; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.end(JSON.stringify(body)); +} + +function isWebhookAuthorized(req: IncomingMessage) { + if (!WEBHOOK_SECRET) return true; + const incoming = String(req.headers["x-telegram-bot-api-secret-token"] || "").trim(); + return incoming !== "" && incoming === WEBHOOK_SECRET; +} + +function isGraphqlAuthorized(req: IncomingMessage) { + if (!GRAPHQL_SHARED_SECRET) return true; + const incoming = String(req.headers["x-graphql-secret"] || "").trim(); + return incoming !== "" && incoming === GRAPHQL_SHARED_SECRET; +} + +async function readJsonBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + let total = 0; + + for await (const chunk of req) { + const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + total += buf.length; + if (total > MAX_BODY_SIZE_BYTES) { + throw new Error(`payload_too_large:${MAX_BODY_SIZE_BYTES}`); + } + chunks.push(buf); + } + + const raw = Buffer.concat(chunks).toString("utf8"); + if (!raw) return {}; + return JSON.parse(raw); +} + +async function sendTelegramMessage(input: { + chatId: string; + text: string; + businessConnectionId?: string | null; +}) { + const token = asString(process.env.TELEGRAM_BOT_TOKEN); + if (!token) { + throw new Error("TELEGRAM_BOT_TOKEN is required"); + } + + const base = String(process.env.TELEGRAM_API_BASE || "https://api.telegram.org").replace(/\/+$/, ""); + const endpoint = `${base}/bot${token}/sendMessage`; + + const payload: Record = { + chat_id: input.chatId, + text: input.text, + }; + if (input.businessConnectionId) { + payload.business_connection_id = input.businessConnectionId; + } + + const response = await fetch(endpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(payload), + }); + + const text = await response.text(); + const body = (() => { + try { + return JSON.parse(text) as Record; + } catch { + return { raw: text }; + } + })(); + + if (!response.ok || body?.ok === false) { + throw new Error(`telegram send failed: ${response.status} ${JSON.stringify(body)}`); + } + + const providerMessageId = + body?.result?.message_id != null + ? String(body.result.message_id) + : body?.message_id != null + ? String(body.message_id) + : null; + + return { + ok: true, + message: "sent", + providerMessageId, + responseJson: JSON.stringify(body), + }; +} + +const root = { + health: () => ({ + ok: true, + service: "telegram_backend", + now: new Date().toISOString(), + }), + + enqueueTelegramOutbound: async ({ input }: { input: any }) => { + const run = await enqueueTelegramOutboundTask({ + omniMessageId: String(input.omniMessageId ?? ""), + chatId: String(input.chatId ?? ""), + text: String(input.text ?? ""), + businessConnectionId: input.businessConnectionId != null ? String(input.businessConnectionId) : null, + }); + + return { + ok: true, + message: "enqueued", + runId: run.runId, + }; + }, + + sendTelegramMessage: async ({ input }: { input: any }) => { + const result = await sendTelegramMessage({ + chatId: String(input.chatId ?? ""), + text: String(input.text ?? ""), + businessConnectionId: input.businessConnectionId != null ? String(input.businessConnectionId) : null, + }); + + return result; + }, +}; + +export function startServer() { + const server = createServer(async (req, res) => { + if (!req.url || !req.method) { + writeJson(res, 404, { ok: false, error: "not_found" }); + return; + } + + if (req.url === "/health" && req.method === "GET") { + writeJson(res, 200, { + ok: true, + service: "telegram_backend", + now: new Date().toISOString(), + }); + return; + } + + if (req.url === "/webhooks/telegram/business" && req.method === "POST") { + if (!isWebhookAuthorized(req)) { + writeJson(res, 401, { ok: false, error: "invalid_webhook_secret" }); + return; + } + + try { + const body = await readJsonBody(req); + const envelope = parseTelegramBusinessUpdate(body); + const run = await enqueueTelegramInboundTask(envelope); + + writeJson(res, 200, { + ok: true, + queued: true, + runId: run.runId, + providerEventId: envelope.providerEventId, + idempotencyKey: envelope.idempotencyKey, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const statusCode = message.startsWith("payload_too_large:") ? 413 : 503; + writeJson(res, statusCode, { + ok: false, + error: "receiver_enqueue_failed", + message, + }); + } + + return; + } + + if (req.url === "/graphql" && req.method === "POST") { + if (!isGraphqlAuthorized(req)) { + writeJson(res, 401, { errors: [{ message: "unauthorized" }] }); + return; + } + + try { + const body = (await readJsonBody(req)) as { + query?: string; + variables?: Record; + operationName?: string; + }; + + const result = await graphql({ + schema, + source: String(body.query || ""), + rootValue: root, + variableValues: body.variables || {}, + operationName: body.operationName, + }); + + writeJson(res, 200, result); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const statusCode = message.startsWith("payload_too_large:") ? 413 : 400; + writeJson(res, statusCode, { errors: [{ message }] }); + } + + return; + } + + writeJson(res, 404, { ok: false, error: "not_found" }); + }); + + server.listen(PORT, "0.0.0.0", () => { + console.log(`[telegram_backend] listening on :${PORT}`); + }); + + return server; +} diff --git a/omni_inbound/src/telegram.ts b/telegram_backend/src/telegram.ts similarity index 100% rename from omni_inbound/src/telegram.ts rename to telegram_backend/src/telegram.ts diff --git a/omni_inbound/src/types.ts b/telegram_backend/src/types.ts similarity index 100% rename from omni_inbound/src/types.ts rename to telegram_backend/src/types.ts diff --git a/omni_inbound/tsconfig.json b/telegram_backend/tsconfig.json similarity index 100% rename from omni_inbound/tsconfig.json rename to telegram_backend/tsconfig.json diff --git a/omni_outbound/.dockerignore b/telegram_worker/.dockerignore similarity index 100% rename from omni_outbound/.dockerignore rename to telegram_worker/.dockerignore diff --git a/telegram_worker/Dockerfile b/telegram_worker/Dockerfile new file mode 100644 index 0000000..b5c8b5e --- /dev/null +++ b/telegram_worker/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY src ./src +COPY tsconfig.json ./tsconfig.json + +ENV NODE_ENV=production + +CMD ["npm", "run", "start"] diff --git a/telegram_worker/README.md b/telegram_worker/README.md new file mode 100644 index 0000000..8869626 --- /dev/null +++ b/telegram_worker/README.md @@ -0,0 +1,30 @@ +# telegram_worker + +Hatchet worker для Telegram-цепочки. + +## Назначение + +- выполняет `process-telegram-inbound`: + - забирает нормализованный inbound envelope; + - пишет событие в `backend` через GraphQL mutation `ingestTelegramInbound`. +- выполняет `process-telegram-outbound`: + - отправляет сообщение через `telegram_backend` mutation `sendTelegramMessage`; + - репортит статус в `backend` mutation `reportTelegramOutbound`. +- ретраи/бекофф выполняются через Hatchet. + +## Переменные окружения + +- `BACKEND_GRAPHQL_URL` (required) +- `BACKEND_GRAPHQL_SHARED_SECRET` (optional) +- `BACKEND_REPORT_RETRIES` (default: `6`) +- `TELEGRAM_BACKEND_GRAPHQL_URL` (required) +- `TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET` (optional) +- `HATCHET_CLIENT_TOKEN` (required) +- `HATCHET_CLIENT_TLS_STRATEGY` (для self-host без TLS: `none`) +- `HATCHET_CLIENT_HOST_PORT` (например, `hatchet-engine:7070`) +- `HATCHET_CLIENT_API_URL` (URL Hatchet API) + +## Скрипты + +- `npm run start` — запуск Hatchet worker. +- `npm run typecheck` — проверка TypeScript. diff --git a/telegram_worker/package-lock.json b/telegram_worker/package-lock.json new file mode 100644 index 0000000..b6e9927 --- /dev/null +++ b/telegram_worker/package-lock.json @@ -0,0 +1,1456 @@ +{ + "name": "crm-telegram-worker", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crm-telegram-worker", + "dependencies": { + "@hatchet-dev/typescript-sdk": "^1.15.2" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^4.20.5", + "typescript": "^5.9.2" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hatchet-dev/typescript-sdk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@hatchet-dev/typescript-sdk/-/typescript-sdk-1.15.2.tgz", + "integrity": "sha512-y9PY8m7thGBoIjJdmM+0x6F8qT8u+ZdrmzOQK3jp/TpCspR8PtsKmVzLc4zQiRBtzzSw0GVJbbA9xZG6zZlz3A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.2.5", + "@types/qs": "^6.9.18", + "abort-controller-x": "^0.4.3", + "axios": "^1.13.5", + "long": "^5.3.1", + "nice-grpc": "^2.1.12", + "nice-grpc-common": "^2.0.2", + "protobufjs": "^7.4.0", + "qs": "^6.14.2", + "semver": "^7.7.1", + "yaml": "^2.7.1", + "zod": "^3.24.2", + "zod-to-json-schema": "^3.24.1" + }, + "optionalDependencies": { + "prom-client": "^15.1.3" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/abort-controller-x": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", + "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT", + "optional": true + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-grpc": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.14.tgz", + "integrity": "sha512-GK9pKNxlvnU5FAdaw7i2FFuR9CqBspcE+if2tqnKXBcE0R8525wj4BZvfcwj7FjvqbssqKxRHt2nwedalbJlww==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.14.0", + "abort-controller-x": "^0.4.0", + "nice-grpc-common": "^2.0.2" + } + }, + "node_modules/nice-grpc-common": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz", + "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", + "license": "MIT", + "dependencies": { + "ts-error": "^1.0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "optional": true, + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/ts-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz", + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "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": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/telegram_worker/package.json b/telegram_worker/package.json new file mode 100644 index 0000000..a93bdf7 --- /dev/null +++ b/telegram_worker/package.json @@ -0,0 +1,17 @@ +{ + "name": "crm-telegram-worker", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/hatchet/worker.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hatchet-dev/typescript-sdk": "^1.15.2" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "tsx": "^4.20.5", + "typescript": "^5.9.2" + } +} diff --git a/telegram_worker/src/hatchet/client.ts b/telegram_worker/src/hatchet/client.ts new file mode 100644 index 0000000..128a422 --- /dev/null +++ b/telegram_worker/src/hatchet/client.ts @@ -0,0 +1,3 @@ +import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1"; + +export const hatchet = HatchetClient.init(); diff --git a/telegram_worker/src/hatchet/tasks.ts b/telegram_worker/src/hatchet/tasks.ts new file mode 100644 index 0000000..b35cffd --- /dev/null +++ b/telegram_worker/src/hatchet/tasks.ts @@ -0,0 +1,347 @@ +import { NonRetryableError } from "@hatchet-dev/typescript-sdk/v1/task"; +import { hatchet } from "./client"; + +type OmniInboundEnvelopeV1 = { + version: number; + idempotencyKey: string; + provider: string; + channel: "TELEGRAM" | "WHATSAPP" | "INSTAGRAM" | "PHONE" | "EMAIL" | "INTERNAL"; + direction: "IN" | "OUT"; + providerEventId: string; + providerMessageId: string | null; + eventType: string; + occurredAt: string; + receivedAt: string; + payloadRaw: unknown; + payloadNormalized: Record; +}; + +type TelegramOutboundTaskInput = { + omniMessageId: string; + chatId: string; + text: string; + businessConnectionId?: string | null; +}; + +type GraphqlResponse = { + data?: T; + errors?: Array<{ message?: string }>; +}; + +type GraphqlRequest = { + query: string; + variables?: Record; + operationName?: string; +}; + +function asString(value: unknown) { + if (typeof value !== "string") return null; + const v = value.trim(); + return v || null; +} + +function requiredEnv(name: string) { + const value = asString(process.env[name]); + if (!value) { + throw new Error(`${name} is required`); + } + return value; +} + +function safeJsonString(value: unknown) { + try { + return JSON.stringify(value ?? null); + } catch { + return JSON.stringify({ unserializable: true }); + } +} + +function compactError(error: unknown) { + if (!error) return "unknown_error"; + if (typeof error === "string") return error; + const anyErr = error as { message?: unknown; stack?: unknown }; + return String(anyErr.message ?? anyErr.stack ?? "unknown_error"); +} + +async function sleep(ms: number) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function callGraphql( + url: string, + body: GraphqlRequest, + secret: string | null, +): Promise { + const headers: Record = { + "content-type": "application/json", + }; + + if (secret) { + headers["x-graphql-secret"] = secret; + } + + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + + const payload = (await response.json()) as GraphqlResponse; + if (!response.ok || payload.errors?.length) { + const message = + payload.errors?.map((error) => error.message).filter(Boolean).join("; ") || `HTTP ${response.status}`; + throw new Error(message); + } + + if (!payload.data) { + throw new Error("graphql_data_missing"); + } + + return payload.data; +} + +async function reportToBackend(input: { + omniMessageId: string; + status: "SENT" | "FAILED"; + providerMessageId?: string | null; + error?: string | null; + responseJson?: string | null; +}) { + type ReportResult = { + reportTelegramOutbound: { + ok: boolean; + message: string; + }; + }; + + const url = requiredEnv("BACKEND_GRAPHQL_URL"); + const secret = asString(process.env.BACKEND_GRAPHQL_SHARED_SECRET); + + const query = `mutation ReportTelegramOutbound($input: TelegramOutboundReportInput!) { + reportTelegramOutbound(input: $input) { + ok + message + } + }`; + + const data = await callGraphql( + url, + { + operationName: "ReportTelegramOutbound", + query, + variables: { + input: { + omniMessageId: input.omniMessageId, + status: input.status, + providerMessageId: input.providerMessageId ?? null, + error: input.error ?? null, + responseJson: input.responseJson ?? null, + }, + }, + }, + secret, + ); + + if (!data.reportTelegramOutbound.ok) { + throw new Error(data.reportTelegramOutbound.message || "backend_report_failed"); + } +} + +async function reportToBackendWithRetry( + input: Parameters[0], + attempts: number, +) { + let lastError: unknown = null; + + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + await reportToBackend(input); + return; + } catch (error) { + lastError = error; + if (attempt < attempts) { + const delayMs = Math.min(30000, 250 * 2 ** attempt); + await sleep(delayMs); + } + } + } + + throw lastError instanceof Error ? lastError : new Error(compactError(lastError)); +} + +async function ingestInboundToBackend(input: OmniInboundEnvelopeV1) { + type IngestResult = { + ingestTelegramInbound: { + ok: boolean; + message: string; + omniMessageId?: string | null; + }; + }; + + const url = requiredEnv("BACKEND_GRAPHQL_URL"); + const secret = asString(process.env.BACKEND_GRAPHQL_SHARED_SECRET); + + const query = `mutation IngestTelegramInbound($input: TelegramInboundInput!) { + ingestTelegramInbound(input: $input) { + ok + message + omniMessageId + } + }`; + + const data = await callGraphql( + url, + { + operationName: "IngestTelegramInbound", + query, + variables: { + input: { + version: input.version, + idempotencyKey: input.idempotencyKey, + provider: input.provider, + channel: input.channel, + direction: input.direction, + providerEventId: input.providerEventId, + providerMessageId: input.providerMessageId, + eventType: input.eventType, + occurredAt: input.occurredAt, + receivedAt: input.receivedAt, + payloadRawJson: safeJsonString(input.payloadRaw), + payloadNormalizedJson: safeJsonString(input.payloadNormalized), + }, + }, + }, + secret, + ); + + if (!data.ingestTelegramInbound.ok) { + throw new Error(data.ingestTelegramInbound.message || "backend_ingest_failed"); + } +} + +async function sendViaTelegramBackend(input: TelegramOutboundTaskInput) { + type SendResult = { + sendTelegramMessage: { + ok: boolean; + message: string; + providerMessageId?: string | null; + responseJson?: string | null; + }; + }; + + const url = requiredEnv("TELEGRAM_BACKEND_GRAPHQL_URL"); + const secret = asString(process.env.TELEGRAM_BACKEND_GRAPHQL_SHARED_SECRET); + + const query = `mutation SendTelegramMessage($input: TelegramSendMessageInput!) { + sendTelegramMessage(input: $input) { + ok + message + providerMessageId + responseJson + } + }`; + + const data = await callGraphql( + url, + { + operationName: "SendTelegramMessage", + query, + variables: { + input: { + chatId: input.chatId, + text: input.text, + businessConnectionId: input.businessConnectionId ?? null, + }, + }, + }, + secret, + ); + + if (!data.sendTelegramMessage.ok) { + throw new Error(data.sendTelegramMessage.message || "telegram_send_failed"); + } + + return { + providerMessageId: data.sendTelegramMessage.providerMessageId ?? null, + responseJson: data.sendTelegramMessage.responseJson ?? null, + }; +} + +export const processTelegramInbound = hatchet.task({ + name: "process-telegram-inbound", + retries: 12, + backoff: { factor: 2, maxSeconds: 60 }, + fn: async (input: any, ctx) => { + const envelope = input as OmniInboundEnvelopeV1; + await ingestInboundToBackend(envelope); + + await ctx.logger.info("telegram inbound processed", { + providerEventId: envelope.providerEventId, + idempotencyKey: envelope.idempotencyKey, + eventType: envelope.eventType, + }); + + return { ok: true }; + }, +}); + +export const processTelegramOutbound = hatchet.task({ + name: "process-telegram-outbound", + retries: 12, + backoff: { factor: 2, maxSeconds: 60 }, + fn: async (input: any, ctx) => { + const payload = input as TelegramOutboundTaskInput; + try { + const sent = await sendViaTelegramBackend(payload); + + const reportAttempts = Math.max(1, Number(process.env.BACKEND_REPORT_RETRIES || 6)); + try { + await reportToBackendWithRetry( + { + omniMessageId: payload.omniMessageId, + status: "SENT", + providerMessageId: sent.providerMessageId, + responseJson: sent.responseJson, + }, + reportAttempts, + ); + } catch (reportError) { + await ctx.logger.error("telegram sent but backend status report failed", { + error: reportError instanceof Error ? reportError : new Error(compactError(reportError)), + extra: { + omniMessageId: payload.omniMessageId, + providerMessageId: sent.providerMessageId, + }, + }); + + throw new NonRetryableError( + `telegram sent but backend report failed: ${compactError(reportError)}`, + ); + } + + await ctx.logger.info("telegram outbound processed", { + omniMessageId: payload.omniMessageId, + providerMessageId: sent.providerMessageId, + }); + + return { + ok: true, + providerMessageId: sent.providerMessageId, + }; + } catch (error) { + const errorMessage = compactError(error); + + try { + await reportToBackend({ + omniMessageId: payload.omniMessageId, + status: "FAILED", + error: errorMessage, + }); + } catch { + // Ignore report failure here: the task itself will retry. + } + + throw error; + } + }, +}); diff --git a/telegram_worker/src/hatchet/worker.ts b/telegram_worker/src/hatchet/worker.ts new file mode 100644 index 0000000..71629ae --- /dev/null +++ b/telegram_worker/src/hatchet/worker.ts @@ -0,0 +1,22 @@ +import { hatchet } from "./client"; +import { processTelegramInbound, processTelegramOutbound } from "./tasks"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +async function main() { + const worker = await hatchet.worker("telegram-worker", { + workflows: [processTelegramInbound, processTelegramOutbound], + }); + + await worker.start(); +} + +const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isMain) { + main().catch((error) => { + const message = error instanceof Error ? error.stack || error.message : String(error); + console.error(`[telegram_worker/hatchet] worker failed: ${message}`); + process.exitCode = 1; + }); +} diff --git a/omni_outbound/tsconfig.json b/telegram_worker/tsconfig.json similarity index 100% rename from omni_outbound/tsconfig.json rename to telegram_worker/tsconfig.json