diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..74861cc --- /dev/null +++ b/api/README.md @@ -0,0 +1,21 @@ +# CMPP Platform API Spike + +阶段 0 的 `api/` 目录用于 NestJS + BullMQ 通信 Spike。当前只定义边界,不展开业务模块。 + +## 阶段 0 职责 + +- 生产 `cmpp.submit.commands` 队列消息。 +- 消费 `cmpp.submit.results`、`cmpp.receipt.events`、`cmpp.uplink.events`。 +- 使用 `traceId`、`messageId`、`channelId` 记录最小链路状态。 +- 为 500 条/秒 Spike 提供批量入队脚本或测试入口。 + +## 不在阶段 0 实现 + +- 认证、权限、租户完整模型。 +- 企业、应用、签名、模板 CRUD。 +- 风控、审核、报备、计费真实业务规则。 +- PostgreSQL/Prisma 正式 schema。 + +## 队列契约 + +以 `docs/contracts/gateway-queue-messages.schema.json` 和 `docs/contracts/examples/` 为准。 diff --git a/api/package-lock.json b/api/package-lock.json new file mode 100644 index 0000000..63c8e06 --- /dev/null +++ b/api/package-lock.json @@ -0,0 +1,3714 @@ +{ + "name": "cmpp-platform-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cmpp-platform-api", + "version": "0.1.0", + "dependencies": { + "@nestjs/common": "^11.1.9", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.9", + "@nestjs/platform-express": "^11.1.9", + "@nestjs/swagger": "^11.2.3", + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.0.1", + "bullmq": "^5.79.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", + "ioredis": "^5.11.1", + "minio": "^8.0.7", + "pg": "^8.22.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "prisma": "^7.0.1", + "ts-node": "^10.9.2", + "typescript": "^6.0.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nestjs/common": { + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.27.tgz", + "integrity": "sha512-kEGSzqM2lWr4whh4Ubflw+oPZSEzxvRMu9WL+LveZploJWTjec5bBlCiRVlVzTPg2kIwBiLwWSvCCW7Wnin1gg==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.4", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz", + "integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==", + "license": "MIT", + "dependencies": { + "dotenv": "17.4.1", + "dotenv-expand": "12.0.3", + "lodash": "4.18.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.27.tgz", + "integrity": "sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.27.tgz", + "integrity": "sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.1.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.5.tgz", + "integrity": "sha512-lvndlJmWBVDOUT0uEtLi6sSpW1syK2/nbAlHBhiELBORMpJGe9+EiWAT9qHtB10jW91L2Jmlwkr0/lttsYZrig==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "4.3.0", + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.8" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", + "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/better-result": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", + "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bullmq": { + "version": "5.79.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.2.tgz", + "integrity": "sha512-FebD+8XCZl/hnS1R4to24L4EAN70XSndKZO0776M36vGRk5MKVhKlNFM8/34zXLXKyYB4QaeIPFhVXSYYGTHpQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "2.0.4", + "node-abort-controller": "3.1.1", + "semver": "7.8.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, + "node_modules/bullmq/node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/bullmq/node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "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/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "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/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "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/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "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/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "devOptional": 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==", + "devOptional": 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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", + "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^1.0.1", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.4.1", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "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/giget": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.0.tgz", + "integrity": "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "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/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-unsafe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", + "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/libphonenumber-js": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.7.tgz", + "integrity": "sha512-rvr3HIMdOgzhz1RFGjftji+wjoAFlzhqCNqJOU/MKTZQ8d9NZxAR/tI+0weDicyoucqVR0U1GCniqHJ0f8aM2A==", + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "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/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "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/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "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/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minio": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz", + "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^1.0.0", + "eventemitter3": "^5.0.1", + "fast-xml-parser": "^5.3.4", + "ipaddr.js": "^2.0.1", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "stream-json": "^1.8.0", + "through2": "^4.0.2", + "xml2js": "^0.5.0 || ^0.6.2" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/minio/node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/minio/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/minio/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/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": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "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.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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/multer/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/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", + "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.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/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + } + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..b8ca47e --- /dev/null +++ b/api/package.json @@ -0,0 +1,37 @@ +{ + "name": "cmpp-platform-api", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "start": "node dist/main.js", + "start:dev": "ts-node src/main.ts", + "prisma:generate": "prisma generate", + "prisma:migrate:dev": "prisma migrate dev", + "prisma:migrate:deploy": "prisma migrate deploy" + }, + "dependencies": { + "@nestjs/common": "^11.1.9", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.9", + "@nestjs/platform-express": "^11.1.9", + "@nestjs/swagger": "^11.2.3", + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.0.1", + "bullmq": "^5.79.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", + "ioredis": "^5.11.1", + "minio": "^8.0.7", + "pg": "^8.22.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "prisma": "^7.0.1", + "ts-node": "^10.9.2", + "typescript": "^6.0.3" + } +} diff --git a/api/prisma.config.ts b/api/prisma.config.ts new file mode 100644 index 0000000..3bc5f8f --- /dev/null +++ b/api/prisma.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'prisma/config'; + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + }, + datasource: { + url: + process.env.DATABASE_URL ?? + 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', + }, +}); diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma new file mode 100644 index 0000000..2470e4f --- /dev/null +++ b/api/prisma/schema.prisma @@ -0,0 +1,601 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" +} + +model Tenant { + id String @id @default(cuid()) + name String + code String @unique + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] + operationLogs OperationLog[] + fileObjects FileObject[] + enterpriseBlacklists EnterpriseBlacklist[] + accounts TenantAccount[] + accountTransactions AccountTransaction[] + rechargeOrders RechargeOrder[] + smsBillingRecords SmsBillingRecord[] + smsApplications SmsApplication[] + smsSignatures SmsSignature[] + smsTemplates SmsTemplate[] + auditRecords AuditRecord[] +} + +model User { + id String @id @default(cuid()) + tenantId String? + username String @unique + displayName String + passwordHash String + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant? @relation(fields: [tenantId], references: [id]) + roles UserRole[] + operationLogs OperationLog[] + auditRecords AuditRecord[] +} + +model Role { + id String @id @default(cuid()) + code String @unique + name String + scope String @default("platform") + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users UserRole[] + permissions RolePermission[] +} + +model Permission { + id String @id @default(cuid()) + code String @unique + name String + description String? + createdAt DateTime @default(now()) + + roles RolePermission[] +} + +model UserRole { + userId String + roleId String + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + + @@id([userId, roleId]) +} + +model RolePermission { + roleId String + permissionId String + + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) + + @@id([roleId, permissionId]) +} + +model OperationLog { + id String @id @default(cuid()) + tenantId String? + userId String? + action String + resource String + resourceId String? + ipAddress String? + userAgent String? + detail Json? + createdAt DateTime @default(now()) + + tenant Tenant? @relation(fields: [tenantId], references: [id]) + user User? @relation(fields: [userId], references: [id]) + + @@index([tenantId, createdAt]) + @@index([userId, createdAt]) +} + +model FileObject { + id String @id @default(cuid()) + tenantId String? + bucket String + objectKey String @unique + fileName String + contentType String + sizeBytes BigInt + checksum String? + purpose String + createdAt DateTime @default(now()) + + tenant Tenant? @relation(fields: [tenantId], references: [id]) +} + +model PhoneSegment { + id String @id @default(cuid()) + prefix String @unique + carrier String + province String? + city String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model SensitiveWord { + id String @id @default(cuid()) + word String @unique + level String @default("block") + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model EnterpriseBlacklist { + id String @id @default(cuid()) + tenantId String + phoneNumber String + reason String? + status String @default("active") + createdAt DateTime @default(now()) + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@unique([tenantId, phoneNumber]) +} + +model GlobalBlacklist { + id String @id @default(cuid()) + phoneNumber String @unique + reason String? + status String @default("active") + createdAt DateTime @default(now()) +} + +model DrainageField { + id String @id @default(cuid()) + code String @unique + name String + fieldType String + required Boolean @default(false) + status String @default("active") + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model BillingPlan { + id String @id @default(cuid()) + name String + priceCents Int + smsUnits Int + validDays Int + status String @default("active") + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + rechargeOrders RechargeOrder[] +} + +model TenantAccount { + id String @id @default(cuid()) + tenantId String + balanceCents Int @default(0) + smsUnits Int @default(0) + creditCents Int @default(0) + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@unique([tenantId]) +} + +model AccountTransaction { + id String @id @default(cuid()) + tenantId String + transactionType String + amountCents Int @default(0) + smsUnits Int @default(0) + balanceAfter Int @default(0) + relatedType String? + relatedId String? + remark String? + createdAt DateTime @default(now()) + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@index([tenantId, createdAt]) + @@index([relatedType, relatedId]) +} + +model BillingRule { + id String @id @default(cuid()) + code String @unique + name String + chargeBasis String @default("submit_success") + unitPrice Int + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model RechargeOrder { + id String @id @default(cuid()) + tenantId String + planId String? + orderNo String @unique + amountCents Int + smsUnits Int @default(0) + status String @default("created") + payMethod String? + paidAt DateTime? + operatorId String? + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id]) + plan BillingPlan? @relation(fields: [planId], references: [id]) + + @@index([tenantId, createdAt]) +} + +model SmsBillingRecord { + id String @id @default(cuid()) + tenantId String + applicationId String? + taskId String? + messageId String? + phoneNumber String? + contentLength Int + billingUnits Int + unitPrice Int + amountCents Int + billingStatus String @default("estimated") + transactionId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@index([tenantId, createdAt]) + @@index([taskId]) + @@index([messageId]) +} + +model SmsApplication { + id String @id @default(cuid()) + tenantId String + name String + scene String? + callbackUrl String? + secretHash String + dailyLimit Int? + maxPhonesPerTask Int @default(1000000) + templateMismatchMode String @default("reject") + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id]) + ipAllowlist SmsApplicationIpAllowlist[] + signatures SmsSignature[] + templates SmsTemplate[] + + @@index([tenantId, status]) +} + +model SmsApplicationIpAllowlist { + id String @id @default(cuid()) + applicationId String + ipCidr String + remark String? + createdAt DateTime @default(now()) + + application SmsApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade) + + @@unique([applicationId, ipCidr]) +} + +model SmsSignature { + id String @id @default(cuid()) + tenantId String + applicationId String? + name String + purpose String? + drainageInfo Json? + auditStatus String @default("draft") + reportStatus String @default("waiting_material") + rejectReason String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id]) + application SmsApplication? @relation(fields: [applicationId], references: [id]) + materials SignatureMaterial[] + templates SmsTemplate[] + reportMaterials SignatureReportMaterial[] + reportTasks ChannelSignatureReportTask[] + + @@index([tenantId, auditStatus]) + @@index([tenantId, reportStatus]) +} + +model SignatureMaterial { + id String @id @default(cuid()) + signatureId String + fileObjectId String? + materialType String + title String + description String? + createdAt DateTime @default(now()) + + signature SmsSignature @relation(fields: [signatureId], references: [id], onDelete: Cascade) +} + +model SmsTemplate { + id String @id @default(cuid()) + tenantId String + applicationId String + signatureId String? + name String + content String + category String? + auditStatus String @default("draft") + rejectReason String? + billingUnits Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id]) + application SmsApplication @relation(fields: [applicationId], references: [id]) + signature SmsSignature? @relation(fields: [signatureId], references: [id]) + variables TemplateVariable[] + + @@index([tenantId, auditStatus]) + @@index([applicationId]) +} + +model TemplateVariable { + id String @id @default(cuid()) + templateId String + name String + example String? + required Boolean @default(true) + createdAt DateTime @default(now()) + + template SmsTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) + + @@unique([templateId, name]) +} + +model AuditRecord { + id String @id @default(cuid()) + tenantId String? + targetType String + targetId String + action String + statusBefore String? + statusAfter String + reason String? + reviewerId String? + createdAt DateTime @default(now()) + + tenant Tenant? @relation(fields: [tenantId], references: [id]) + reviewer User? @relation(fields: [reviewerId], references: [id]) + + @@index([targetType, targetId]) + @@index([tenantId, createdAt]) +} + +model SmsChannel { + id String @id @default(cuid()) + code String @unique + name String + carrier String? + protocol String @default("CMPP") + gatewayHost String + gatewayPort Int + enterpriseCode String? + account String + passwordCipher String + srcId String + cmppVersion String @default("3.0") + rateLimitPerSecond Int @default(100) + unitPrice Int @default(0) + status String @default("active") + config Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + groupItems SmsChannelGroupItem[] + routeRules ChannelRouteRule[] + healthMetrics ChannelHealthMetric[] + reportFields ChannelReportField[] + reportTasks ChannelSignatureReportTask[] + reportRecords ChannelSignatureReportRecord[] + + @@index([status]) +} + +model SmsChannelGroup { + id String @id @default(cuid()) + code String @unique + name String + description String? + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + items SmsChannelGroupItem[] + routeRules ChannelRouteRule[] +} + +model SmsChannelGroupItem { + id String @id @default(cuid()) + groupId String + channelId String + carrier String? + province String? + priority Int @default(100) + weight Int @default(1) + isBackup Boolean @default(false) + rateLimitPerSecond Int? + createdAt DateTime @default(now()) + + group SmsChannelGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) + channel SmsChannel @relation(fields: [channelId], references: [id]) + + @@unique([groupId, channelId, carrier, province]) + @@index([channelId]) +} + +model ChannelRouteRule { + id String @id @default(cuid()) + tenantId String? + applicationId String? + groupId String + channelId String? + carrier String? + province String? + priority Int @default(100) + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + group SmsChannelGroup @relation(fields: [groupId], references: [id]) + channel SmsChannel? @relation(fields: [channelId], references: [id]) + + @@index([tenantId, applicationId, status]) + @@index([groupId, priority]) +} + +model ChannelHealthMetric { + id String @id @default(cuid()) + channelId String + submitTotal Int @default(0) + submitSuccess Int @default(0) + submitFailed Int @default(0) + receiptSuccess Int @default(0) + receiptFailed Int @default(0) + unknownTotal Int @default(0) + windowStart DateTime + windowEnd DateTime + createdAt DateTime @default(now()) + + channel SmsChannel @relation(fields: [channelId], references: [id]) + + @@index([channelId, windowStart]) +} + +model ChannelReportField { + id String @id @default(cuid()) + channelId String + code String + name String + fieldType String + required Boolean @default(false) + description String? + sortOrder Int @default(100) + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + channel SmsChannel @relation(fields: [channelId], references: [id], onDelete: Cascade) + + @@unique([channelId, code]) +} + +model SignatureReportMaterial { + id String @id @default(cuid()) + signatureId String + channelId String + fieldCode String + fieldValue String? + fileObjectId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + signature SmsSignature @relation(fields: [signatureId], references: [id], onDelete: Cascade) + + @@unique([signatureId, channelId, fieldCode]) + @@index([channelId]) +} + +model ChannelSignatureReportTask { + id String @id @default(cuid()) + tenantId String + signatureId String + channelId String + status String @default("pending") + reason String? + createdById String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + signature SmsSignature @relation(fields: [signatureId], references: [id]) + channel SmsChannel @relation(fields: [channelId], references: [id]) + records ChannelSignatureReportRecord[] + exportFiles ReportExportFile[] + receiptImports ReportReceiptImport[] + + @@index([tenantId, status]) + @@index([signatureId, channelId]) +} + +model ChannelSignatureReportRecord { + id String @id @default(cuid()) + taskId String + channelId String + action String + statusBefore String? + statusAfter String + reason String? + operatorId String? + createdAt DateTime @default(now()) + + task ChannelSignatureReportTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + channel SmsChannel @relation(fields: [channelId], references: [id]) + + @@index([taskId, createdAt]) + @@index([channelId, createdAt]) +} + +model ReportExportFile { + id String @id @default(cuid()) + taskId String + fileObjectId String? + fileName String + rowCount Int @default(0) + status String @default("generated") + createdAt DateTime @default(now()) + + task ChannelSignatureReportTask @relation(fields: [taskId], references: [id], onDelete: Cascade) +} + +model ReportReceiptImport { + id String @id @default(cuid()) + taskId String + fileObjectId String? + fileName String + rowCount Int @default(0) + successCount Int @default(0) + failedCount Int @default(0) + status String @default("imported") + result Json? + createdAt DateTime @default(now()) + + task ChannelSignatureReportTask @relation(fields: [taskId], references: [id], onDelete: Cascade) +} diff --git a/api/src/app.module.ts b/api/src/app.module.ts new file mode 100644 index 0000000..2c8ef4e --- /dev/null +++ b/api/src/app.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { AuditModule } from './audit/audit.module'; +import { AuthModule } from './auth/auth.module'; +import { BillingModule } from './billing/billing.module'; +import { ChannelsModule } from './channels/channels.module'; +import { DictionariesModule } from './dictionaries/dictionaries.module'; +import { FilesModule } from './files/files.module'; +import { HealthController } from './health.controller'; +import { PrismaModule } from './prisma/prisma.module'; +import { SmsConfigModule } from './sms-config/sms-config.module'; +import { TenantsModule } from './tenants/tenants.module'; +import { UsersModule } from './users/users.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env.local', '.env'], + }), + PrismaModule, + AuthModule, + TenantsModule, + UsersModule, + AuditModule, + FilesModule, + DictionariesModule, + BillingModule, + SmsConfigModule, + ChannelsModule, + ], + controllers: [HealthController], +}) +export class AppModule {} diff --git a/api/src/audit/audit.controller.ts b/api/src/audit/audit.controller.ts new file mode 100644 index 0000000..1f51f69 --- /dev/null +++ b/api/src/audit/audit.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantId } from '../common/tenant-id.decorator'; +import { AuditService, CreateOperationLogDto } from './audit.service'; + +@ApiTags('audit') +@Controller('admin/operation-logs') +export class AuditController { + constructor(private readonly audit: AuditService) {} + + @Get() + list(@TenantId() tenantId?: string) { + return this.audit.list(tenantId); + } + + @Post() + create(@Body() body: CreateOperationLogDto) { + return this.audit.create(body); + } +} diff --git a/api/src/audit/audit.module.ts b/api/src/audit/audit.module.ts new file mode 100644 index 0000000..3695111 --- /dev/null +++ b/api/src/audit/audit.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuditController } from './audit.controller'; +import { AuditService } from './audit.service'; + +@Module({ + controllers: [AuditController], + providers: [AuditService], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/api/src/audit/audit.service.ts b/api/src/audit/audit.service.ts new file mode 100644 index 0000000..aebd095 --- /dev/null +++ b/api/src/audit/audit.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreateOperationLogDto { + tenantId?: string; + userId?: string; + action: string; + resource: string; + resourceId?: string; + ipAddress?: string; + userAgent?: string; + detail?: Record; +} + +@Injectable() +export class AuditService { + constructor(private readonly prisma: PrismaService) {} + + list(tenantId?: string) { + return this.prisma.operationLog.findMany({ + where: tenantId ? { tenantId } : undefined, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + create(data: CreateOperationLogDto) { + const createData: Prisma.OperationLogUncheckedCreateInput = { + tenantId: data.tenantId, + userId: data.userId, + action: data.action, + resource: data.resource, + resourceId: data.resourceId, + ipAddress: data.ipAddress, + userAgent: data.userAgent, + detail: data.detail as Prisma.InputJsonValue | undefined, + }; + return this.prisma.operationLog.create({ data: createData }); + } +} diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts new file mode 100644 index 0000000..f5cd488 --- /dev/null +++ b/api/src/auth/auth.controller.ts @@ -0,0 +1,14 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthService, LoginDto } from './auth.service'; + +@ApiTags('auth') +@Controller('client/auth') +export class AuthController { + constructor(private readonly auth: AuthService) {} + + @Post('login') + login(@Body() body: LoginDto) { + return this.auth.login(body); + } +} diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts new file mode 100644 index 0000000..ecbcc3a --- /dev/null +++ b/api/src/auth/auth.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersModule } from '../users/users.module'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; + +@Module({ + imports: [UsersModule], + controllers: [AuthController], + providers: [AuthService], +}) +export class AuthModule {} diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts new file mode 100644 index 0000000..5f24db3 --- /dev/null +++ b/api/src/auth/auth.service.ts @@ -0,0 +1,30 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { hashPassword, UsersService } from '../users/users.service'; + +export interface LoginDto { + username: string; + password: string; +} + +@Injectable() +export class AuthService { + constructor(private readonly users: UsersService) {} + + async login(data: LoginDto) { + const user = await this.users.findByUsername(data.username); + if (!user || user.passwordHash !== hashPassword(data.password) || user.status !== 'active') { + throw new UnauthorizedException('Invalid username or password'); + } + + return { + accessToken: `dev-token-${user.id}`, + tokenType: 'Bearer', + user: { + id: user.id, + tenantId: user.tenantId, + username: user.username, + displayName: user.displayName, + }, + }; + } +} diff --git a/api/src/billing/billing.controller.ts b/api/src/billing/billing.controller.ts new file mode 100644 index 0000000..7a5b361 --- /dev/null +++ b/api/src/billing/billing.controller.ts @@ -0,0 +1,146 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantId } from '../common/tenant-id.decorator'; +import { + BillingService, + BillingActionDto, + CreateRechargeOrderDto, + CreateAccountTransactionDto, + CreateBillingPlanDto, + CreateBillingRuleDto, + CreateSmsBillingRecordDto, + CreateTenantAccountDto, + EstimateSmsCostDto, +} from './billing.service'; + +@ApiTags('billing') +@Controller('admin/billing') +export class BillingController { + constructor(private readonly billing: BillingService) {} + + @Get('plans') + listPlans() { + return this.billing.listPlans(); + } + + @Post('plans') + createPlan(@Body() body: CreateBillingPlanDto) { + return this.billing.createPlan(body); + } + + @Get('accounts') + listAccounts() { + return this.billing.listAccounts(); + } + + @Post('accounts') + createAccount(@Body() body: CreateTenantAccountDto) { + return this.billing.createAccount(body); + } + + @Get('transactions') + listTransactions(@TenantId() tenantId?: string) { + return this.billing.listTransactions(tenantId); + } + + @Post('transactions') + createTransaction(@Body() body: CreateAccountTransactionDto) { + return this.billing.createTransaction(body); + } + + @Get('recharges') + listRechargeOrders(@TenantId() tenantId?: string) { + return this.billing.listRechargeOrders(tenantId); + } + + @Post('recharges') + createRechargeOrder(@Body() body: CreateRechargeOrderDto) { + return this.billing.createRechargeOrder(body); + } + + @Post('estimate') + estimateSmsCost(@Body() body: EstimateSmsCostDto) { + return this.billing.estimateSmsCost(body); + } + + @Post('check') + checkAccount(@Body() body: BillingActionDto) { + return this.billing.checkAccount(body); + } + + @Post('freeze') + freeze(@Body() body: BillingActionDto) { + return this.billing.freeze(body); + } + + @Post('charge') + charge(@Body() body: BillingActionDto) { + return this.billing.charge(body); + } + + @Post('release') + release(@Body() body: BillingActionDto) { + return this.billing.release(body); + } + + @Post('refund') + refund(@Body() body: BillingActionDto) { + return this.billing.refund(body); + } + + @Post('adjust') + adjust(@Body() body: BillingActionDto) { + return this.billing.adjust(body); + } + + @Get('sms-billing-records') + listSmsBillingRecords(@TenantId() tenantId?: string) { + return this.billing.listSmsBillingRecords(tenantId); + } + + @Post('sms-billing-records') + createSmsBillingRecord(@Body() body: CreateSmsBillingRecordDto) { + return this.billing.createSmsBillingRecord(body); + } + + @Get('rules') + listRules() { + return this.billing.listRules(); + } + + @Post('rules') + createRule(@Body() body: CreateBillingRuleDto) { + return this.billing.createRule(body); + } +} + +@ApiTags('client-billing') +@Controller('client/billing') +export class ClientBillingController { + constructor(private readonly billing: BillingService) {} + + @Get('plans') + listPlans() { + return this.billing.listPlans(); + } + + @Get('transactions') + listTransactions(@TenantId() tenantId?: string) { + return this.billing.listTransactions(tenantId); + } + + @Post('orders') + createRechargeOrder(@Body() body: CreateRechargeOrderDto) { + return this.billing.createRechargeOrder(body); + } + + @Get('orders') + listRechargeOrders(@TenantId() tenantId?: string) { + return this.billing.listRechargeOrders(tenantId); + } + + @Post('estimate') + estimateSmsCost(@Body() body: EstimateSmsCostDto) { + return this.billing.estimateSmsCost(body); + } +} diff --git a/api/src/billing/billing.module.ts b/api/src/billing/billing.module.ts new file mode 100644 index 0000000..f61cc4e --- /dev/null +++ b/api/src/billing/billing.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BillingController, ClientBillingController } from './billing.controller'; +import { BillingService } from './billing.service'; + +@Module({ + controllers: [BillingController, ClientBillingController], + providers: [BillingService], + exports: [BillingService], +}) +export class BillingModule {} diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts new file mode 100644 index 0000000..777f64d --- /dev/null +++ b/api/src/billing/billing.service.ts @@ -0,0 +1,349 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreateBillingPlanDto { + name: string; + priceCents: number; + smsUnits: number; + validDays: number; + status?: string; + description?: string; +} + +export interface CreateTenantAccountDto { + tenantId: string; + balanceCents?: number; + smsUnits?: number; + creditCents?: number; + status?: string; +} + +export interface CreateAccountTransactionDto { + tenantId: string; + transactionType: string; + amountCents?: number; + smsUnits?: number; + balanceAfter?: number; + relatedType?: string; + relatedId?: string; + remark?: string; +} + +export interface CreateBillingRuleDto { + code: string; + name: string; + chargeBasis?: string; + unitPrice: number; + status?: string; +} + +export interface CreateRechargeOrderDto { + tenantId: string; + planId?: string; + amountCents?: number; + smsUnits?: number; + payMethod?: string; + operatorId?: string; + remark?: string; +} + +export interface EstimateSmsCostDto { + tenantId: string; + applicationId?: string; + content: string; + phoneCount: number; + unitPrice?: number; + taskId?: string; +} + +export interface BillingActionDto { + tenantId: string; + amountCents?: number; + smsUnits?: number; + relatedType?: string; + relatedId?: string; + remark?: string; +} + +export interface CreateSmsBillingRecordDto { + tenantId: string; + applicationId?: string; + taskId?: string; + messageId?: string; + phoneNumber?: string; + content: string; + unitPrice?: number; +} + +@Injectable() +export class BillingService { + constructor(private readonly prisma: PrismaService) {} + + listPlans() { + return this.prisma.billingPlan.findMany({ orderBy: { createdAt: 'desc' }, take: 100 }); + } + + createPlan(data: CreateBillingPlanDto) { + return this.prisma.billingPlan.create({ + data: { + name: data.name, + priceCents: data.priceCents, + smsUnits: data.smsUnits, + validDays: data.validDays, + status: data.status ?? 'active', + description: data.description, + }, + }); + } + + listAccounts() { + return this.prisma.tenantAccount.findMany({ + include: { tenant: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createAccount(data: CreateTenantAccountDto) { + const createData: Prisma.TenantAccountUncheckedCreateInput = { + tenantId: data.tenantId, + balanceCents: data.balanceCents ?? 0, + smsUnits: data.smsUnits ?? 0, + creditCents: data.creditCents ?? 0, + status: data.status ?? 'active', + }; + return this.prisma.tenantAccount.create({ data: createData }); + } + + listTransactions(tenantId?: string) { + return this.prisma.accountTransaction.findMany({ + where: tenantId ? { tenantId } : undefined, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createTransaction(data: CreateAccountTransactionDto) { + const createData: Prisma.AccountTransactionUncheckedCreateInput = { + tenantId: data.tenantId, + transactionType: data.transactionType, + amountCents: data.amountCents ?? 0, + smsUnits: data.smsUnits ?? 0, + balanceAfter: data.balanceAfter ?? 0, + relatedType: data.relatedType, + relatedId: data.relatedId, + remark: data.remark, + }; + return this.prisma.accountTransaction.create({ data: createData }); + } + + listRechargeOrders(tenantId?: string) { + return this.prisma.rechargeOrder.findMany({ + where: tenantId ? { tenantId } : undefined, + include: { plan: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + async createRechargeOrder(data: CreateRechargeOrderDto) { + const plan = data.planId ? await this.prisma.billingPlan.findUnique({ where: { id: data.planId } }) : null; + const amountCents = data.amountCents ?? plan?.priceCents ?? 0; + const smsUnits = data.smsUnits ?? plan?.smsUnits ?? 0; + const order = await this.prisma.rechargeOrder.create({ + data: { + tenantId: data.tenantId, + planId: data.planId, + orderNo: `R${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`, + amountCents, + smsUnits, + status: 'paid', + payMethod: data.payMethod ?? 'manual', + paidAt: new Date(), + operatorId: data.operatorId, + remark: data.remark, + }, + }); + + await this.applyAccountDelta({ + tenantId: data.tenantId, + transactionType: 'recharge', + amountCents, + smsUnits, + relatedType: 'recharge_order', + relatedId: order.id, + remark: data.remark, + }); + + return order; + } + + estimateSmsCost(data: EstimateSmsCostDto) { + const billingUnits = estimateBillingUnits(data.content); + const unitPrice = data.unitPrice ?? 0; + const totalUnits = billingUnits * data.phoneCount; + return { + tenantId: data.tenantId, + applicationId: data.applicationId, + taskId: data.taskId, + contentLength: [...data.content].length, + phoneCount: data.phoneCount, + billingUnitsPerMessage: billingUnits, + totalBillingUnits: totalUnits, + unitPrice, + amountCents: totalUnits * unitPrice, + }; + } + + async checkAccount(data: BillingActionDto) { + const account = await this.getAccountOrCreate(data.tenantId); + const requiredAmount = data.amountCents ?? 0; + const requiredUnits = data.smsUnits ?? 0; + const availableAmount = account.balanceCents + account.creditCents; + return { + tenantId: data.tenantId, + requiredAmount, + requiredUnits, + availableAmount, + availableSmsUnits: account.smsUnits, + canSend: availableAmount >= requiredAmount && account.smsUnits >= requiredUnits, + }; + } + + freeze(data: BillingActionDto) { + return this.applyAccountDelta({ + ...data, + transactionType: 'frozen', + amountCents: -(data.amountCents ?? 0), + smsUnits: -(data.smsUnits ?? 0), + }); + } + + charge(data: BillingActionDto) { + return this.applyAccountDelta({ + ...data, + transactionType: 'charged', + amountCents: -(data.amountCents ?? 0), + smsUnits: -(data.smsUnits ?? 0), + }); + } + + release(data: BillingActionDto) { + return this.applyAccountDelta({ + ...data, + transactionType: 'released', + amountCents: data.amountCents ?? 0, + smsUnits: data.smsUnits ?? 0, + }); + } + + refund(data: BillingActionDto) { + return this.applyAccountDelta({ + ...data, + transactionType: 'refunded', + amountCents: data.amountCents ?? 0, + smsUnits: data.smsUnits ?? 0, + }); + } + + adjust(data: BillingActionDto) { + return this.applyAccountDelta({ + ...data, + transactionType: 'adjusted', + amountCents: data.amountCents ?? 0, + smsUnits: data.smsUnits ?? 0, + }); + } + + async createSmsBillingRecord(data: CreateSmsBillingRecordDto) { + const estimate = this.estimateSmsCost({ + tenantId: data.tenantId, + applicationId: data.applicationId, + taskId: data.taskId, + content: data.content, + phoneCount: 1, + unitPrice: data.unitPrice ?? 0, + }); + return this.prisma.smsBillingRecord.create({ + data: { + tenantId: data.tenantId, + applicationId: data.applicationId, + taskId: data.taskId, + messageId: data.messageId, + phoneNumber: data.phoneNumber, + contentLength: estimate.contentLength, + billingUnits: estimate.billingUnitsPerMessage, + unitPrice: estimate.unitPrice, + amountCents: estimate.amountCents, + billingStatus: 'estimated', + }, + }); + } + + listSmsBillingRecords(tenantId?: string, taskId?: string) { + return this.prisma.smsBillingRecord.findMany({ + where: { tenantId, taskId }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + listRules() { + return this.prisma.billingRule.findMany({ orderBy: { createdAt: 'desc' }, take: 100 }); + } + + createRule(data: CreateBillingRuleDto) { + return this.prisma.billingRule.create({ + data: { + code: data.code, + name: data.name, + chargeBasis: data.chargeBasis ?? 'submit_success', + unitPrice: data.unitPrice, + status: data.status ?? 'active', + }, + }); + } + + private async getAccountOrCreate(tenantId: string) { + return this.prisma.tenantAccount.upsert({ + where: { tenantId }, + update: {}, + create: { tenantId, balanceCents: 0, smsUnits: 0, creditCents: 0, status: 'active' }, + }); + } + + private async applyAccountDelta(data: CreateAccountTransactionDto) { + const account = await this.getAccountOrCreate(data.tenantId); + const nextBalance = account.balanceCents + (data.amountCents ?? 0); + const nextUnits = account.smsUnits + (data.smsUnits ?? 0); + await this.prisma.tenantAccount.update({ + where: { tenantId: data.tenantId }, + data: { + balanceCents: nextBalance, + smsUnits: nextUnits, + }, + }); + + return this.prisma.accountTransaction.create({ + data: { + tenantId: data.tenantId, + transactionType: data.transactionType, + amountCents: data.amountCents ?? 0, + smsUnits: data.smsUnits ?? 0, + balanceAfter: nextBalance, + relatedType: data.relatedType, + relatedId: data.relatedId, + remark: data.remark, + }, + }); + } +} + +function estimateBillingUnits(content: string) { + const length = [...content].length; + if (length <= 70) { + return 1; + } + return Math.ceil(length / 67); +} diff --git a/api/src/channels/channels.controller.ts b/api/src/channels/channels.controller.ts new file mode 100644 index 0000000..3a226b4 --- /dev/null +++ b/api/src/channels/channels.controller.ts @@ -0,0 +1,110 @@ +import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + ChannelsService, + CreateChannelDto, + CreateChannelGroupDto, + CreateChannelGroupItemDto, + CreateReceiptImportDto, + CreateReportExportDto, + CreateReportFieldDto, + CreateReportMaterialDto, + CreateReportTaskDto, + CreateRouteRuleDto, +} from './channels.service'; + +@ApiTags('channels') +@Controller('admin') +export class ChannelsController { + constructor(private readonly channels: ChannelsService) {} + + @Get('channels') + listChannels() { + return this.channels.listChannels(); + } + + @Post('channels') + createChannel(@Body() body: CreateChannelDto) { + return this.channels.createChannel(body); + } + + @Post('channels/:id/test') + testChannel(@Param('id') channelId: string) { + return this.channels.testChannel(channelId); + } + + @Get('channels/:id/metrics') + listChannelMetrics(@Param('id') channelId: string) { + return this.channels.listChannelMetrics(channelId); + } + + @Get('channel-groups') + listGroups() { + return this.channels.listGroups(); + } + + @Post('channel-groups') + createGroup(@Body() body: CreateChannelGroupDto) { + return this.channels.createGroup(body); + } + + @Post('channel-groups/items') + addGroupItem(@Body() body: CreateChannelGroupItemDto) { + return this.channels.addGroupItem(body); + } + + @Get('channel-route-rules') + listRouteRules() { + return this.channels.listRouteRules(); + } + + @Post('channel-route-rules') + createRouteRule(@Body() body: CreateRouteRuleDto) { + return this.channels.createRouteRule(body); + } + + @Get('channel-report-fields') + listReportFields(@Query('channelId') channelId?: string) { + return this.channels.listReportFields(channelId); + } + + @Post('channel-report-fields') + createReportField(@Body() body: CreateReportFieldDto) { + return this.channels.createReportField(body); + } + + @Get('signature-report-materials') + listReportMaterials(@Query('signatureId') signatureId?: string, @Query('channelId') channelId?: string) { + return this.channels.listReportMaterials(signatureId, channelId); + } + + @Post('signature-report-materials') + upsertReportMaterial(@Body() body: CreateReportMaterialDto) { + return this.channels.upsertReportMaterial(body); + } + + @Get('report-tasks') + listReportTasks(@Query('tenantId') tenantId?: string, @Query('status') status?: string) { + return this.channels.listReportTasks(tenantId, status); + } + + @Post('report-tasks/generate') + createReportTask(@Body() body: CreateReportTaskDto) { + return this.channels.createReportTask(body); + } + + @Post('report-tasks/:id/export') + createReportExport(@Param('id') taskId: string, @Body() body: CreateReportExportDto) { + return this.channels.createReportExport(taskId, body); + } + + @Post('report-tasks/:id/receipt-import') + importReportReceipt(@Param('id') taskId: string, @Body() body: CreateReceiptImportDto) { + return this.channels.importReportReceipt(taskId, body); + } + + @Get('report-records') + listReportRecords(@Query('taskId') taskId?: string, @Query('channelId') channelId?: string) { + return this.channels.listReportRecords(taskId, channelId); + } +} diff --git a/api/src/channels/channels.module.ts b/api/src/channels/channels.module.ts new file mode 100644 index 0000000..b6c95c4 --- /dev/null +++ b/api/src/channels/channels.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ChannelsController } from './channels.controller'; +import { ChannelsService } from './channels.service'; + +@Module({ + controllers: [ChannelsController], + providers: [ChannelsService], + exports: [ChannelsService], +}) +export class ChannelsModule {} diff --git a/api/src/channels/channels.service.ts b/api/src/channels/channels.service.ts new file mode 100644 index 0000000..6d84f39 --- /dev/null +++ b/api/src/channels/channels.service.ts @@ -0,0 +1,365 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreateChannelDto { + code: string; + name: string; + carrier?: string; + protocol?: string; + gatewayHost: string; + gatewayPort: number; + enterpriseCode?: string; + account: string; + passwordCipher: string; + srcId: string; + cmppVersion?: string; + rateLimitPerSecond?: number; + unitPrice?: number; + status?: string; + config?: Record; +} + +export interface CreateChannelGroupDto { + code: string; + name: string; + description?: string; + status?: string; +} + +export interface CreateChannelGroupItemDto { + groupId: string; + channelId: string; + carrier?: string; + province?: string; + priority?: number; + weight?: number; + isBackup?: boolean; + rateLimitPerSecond?: number; +} + +export interface CreateRouteRuleDto { + tenantId?: string; + applicationId?: string; + groupId: string; + channelId?: string; + carrier?: string; + province?: string; + priority?: number; + status?: string; +} + +export interface CreateReportFieldDto { + channelId: string; + code: string; + name: string; + fieldType: string; + required?: boolean; + description?: string; + sortOrder?: number; + status?: string; +} + +export interface CreateReportMaterialDto { + signatureId: string; + channelId: string; + fieldCode: string; + fieldValue?: string; + fileObjectId?: string; +} + +export interface CreateReportTaskDto { + tenantId: string; + signatureId: string; + channelId: string; + createdById?: string; +} + +export interface CreateReportExportDto { + fileObjectId?: string; + fileName: string; + rowCount?: number; +} + +export interface CreateReceiptImportDto { + fileObjectId?: string; + fileName: string; + rowCount?: number; + successCount?: number; + failedCount?: number; + statusAfter?: string; + reason?: string; + result?: Record; +} + +@Injectable() +export class ChannelsService { + constructor(private readonly prisma: PrismaService) {} + + listChannels() { + return this.prisma.smsChannel.findMany({ orderBy: { createdAt: 'desc' }, take: 100 }); + } + + createChannel(data: CreateChannelDto) { + return this.prisma.smsChannel.create({ + data: { + code: data.code, + name: data.name, + carrier: data.carrier, + protocol: data.protocol ?? 'CMPP', + gatewayHost: data.gatewayHost, + gatewayPort: data.gatewayPort, + enterpriseCode: data.enterpriseCode, + account: data.account, + passwordCipher: data.passwordCipher, + srcId: data.srcId, + cmppVersion: data.cmppVersion ?? '3.0', + rateLimitPerSecond: data.rateLimitPerSecond ?? 100, + unitPrice: data.unitPrice ?? 0, + status: data.status ?? 'active', + config: data.config as Prisma.InputJsonValue | undefined, + }, + }); + } + + testChannel(channelId: string) { + return { + channelId, + status: 'queued', + message: 'Channel test request accepted as a phase-4 placeholder.', + }; + } + + listChannelMetrics(channelId: string) { + return this.prisma.channelHealthMetric.findMany({ + where: { channelId }, + orderBy: { windowStart: 'desc' }, + take: 100, + }); + } + + listGroups() { + return this.prisma.smsChannelGroup.findMany({ + include: { items: { include: { channel: true } } }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createGroup(data: CreateChannelGroupDto) { + return this.prisma.smsChannelGroup.create({ + data: { + code: data.code, + name: data.name, + description: data.description, + status: data.status ?? 'active', + }, + }); + } + + addGroupItem(data: CreateChannelGroupItemDto) { + return this.prisma.smsChannelGroupItem.create({ + data: { + groupId: data.groupId, + channelId: data.channelId, + carrier: data.carrier, + province: data.province, + priority: data.priority ?? 100, + weight: data.weight ?? 1, + isBackup: data.isBackup ?? false, + rateLimitPerSecond: data.rateLimitPerSecond, + }, + }); + } + + listRouteRules() { + return this.prisma.channelRouteRule.findMany({ + include: { group: true, channel: true }, + orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }], + take: 100, + }); + } + + createRouteRule(data: CreateRouteRuleDto) { + return this.prisma.channelRouteRule.create({ + data: { + tenantId: data.tenantId, + applicationId: data.applicationId, + groupId: data.groupId, + channelId: data.channelId, + carrier: data.carrier, + province: data.province, + priority: data.priority ?? 100, + status: data.status ?? 'active', + }, + }); + } + + listReportFields(channelId?: string) { + return this.prisma.channelReportField.findMany({ + where: channelId ? { channelId } : undefined, + orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }], + take: 200, + }); + } + + createReportField(data: CreateReportFieldDto) { + return this.prisma.channelReportField.create({ + data: { + channelId: data.channelId, + code: data.code, + name: data.name, + fieldType: data.fieldType, + required: data.required ?? false, + description: data.description, + sortOrder: data.sortOrder ?? 100, + status: data.status ?? 'active', + }, + }); + } + + listReportMaterials(signatureId?: string, channelId?: string) { + return this.prisma.signatureReportMaterial.findMany({ + where: { + signatureId, + channelId, + }, + orderBy: { createdAt: 'desc' }, + take: 200, + }); + } + + upsertReportMaterial(data: CreateReportMaterialDto) { + return this.prisma.signatureReportMaterial.upsert({ + where: { + signatureId_channelId_fieldCode: { + signatureId: data.signatureId, + channelId: data.channelId, + fieldCode: data.fieldCode, + }, + }, + update: { + fieldValue: data.fieldValue, + fileObjectId: data.fileObjectId, + }, + create: { + signatureId: data.signatureId, + channelId: data.channelId, + fieldCode: data.fieldCode, + fieldValue: data.fieldValue, + fileObjectId: data.fileObjectId, + }, + }); + } + + listReportTasks(tenantId?: string, status?: string) { + return this.prisma.channelSignatureReportTask.findMany({ + where: { tenantId, status }, + include: { signature: true, channel: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + async createReportTask(data: CreateReportTaskDto) { + const task = await this.prisma.channelSignatureReportTask.create({ + data: { + tenantId: data.tenantId, + signatureId: data.signatureId, + channelId: data.channelId, + createdById: data.createdById, + status: 'pending', + }, + }); + await this.recordReportTask(task.id, task.channelId, 'create', undefined, 'pending'); + return task; + } + + async createReportExport(taskId: string, data: CreateReportExportDto) { + const task = await this.getReportTaskOrThrow(taskId); + const exported = await this.prisma.reportExportFile.create({ + data: { + taskId, + fileObjectId: data.fileObjectId, + fileName: data.fileName, + rowCount: data.rowCount ?? 0, + }, + }); + await this.updateReportTaskStatus(taskId, task.channelId, task.status, 'exporting', 'export'); + return exported; + } + + async importReportReceipt(taskId: string, data: CreateReceiptImportDto) { + const task = await this.getReportTaskOrThrow(taskId); + const statusAfter = data.statusAfter ?? (data.failedCount && data.failedCount > 0 ? 'rejected' : 'approved'); + const imported = await this.prisma.reportReceiptImport.create({ + data: { + taskId, + fileObjectId: data.fileObjectId, + fileName: data.fileName, + rowCount: data.rowCount ?? 0, + successCount: data.successCount ?? 0, + failedCount: data.failedCount ?? 0, + status: 'imported', + result: data.result as Prisma.InputJsonValue | undefined, + }, + }); + await this.updateReportTaskStatus(taskId, task.channelId, task.status, statusAfter, 'receipt_import', data.reason); + await this.prisma.smsSignature.update({ + where: { id: task.signatureId }, + data: { reportStatus: statusAfter }, + }); + return imported; + } + + listReportRecords(taskId?: string, channelId?: string) { + return this.prisma.channelSignatureReportRecord.findMany({ + where: { taskId, channelId }, + orderBy: { createdAt: 'desc' }, + take: 200, + }); + } + + private async getReportTaskOrThrow(taskId: string) { + const task = await this.prisma.channelSignatureReportTask.findUnique({ where: { id: taskId } }); + if (!task) { + throw new NotFoundException('Report task not found'); + } + return task; + } + + private async updateReportTaskStatus( + taskId: string, + channelId: string, + statusBefore: string, + statusAfter: string, + action: string, + reason?: string, + ) { + await this.prisma.channelSignatureReportTask.update({ + where: { id: taskId }, + data: { status: statusAfter, reason }, + }); + await this.recordReportTask(taskId, channelId, action, statusBefore, statusAfter, reason); + } + + private recordReportTask( + taskId: string, + channelId: string, + action: string, + statusBefore: string | undefined, + statusAfter: string, + reason?: string, + ) { + return this.prisma.channelSignatureReportRecord.create({ + data: { + taskId, + channelId, + action, + statusBefore, + statusAfter, + reason, + }, + }); + } +} diff --git a/api/src/common/tenant-id.decorator.ts b/api/src/common/tenant-id.decorator.ts new file mode 100644 index 0000000..0341edc --- /dev/null +++ b/api/src/common/tenant-id.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const TenantId = createParamDecorator((_: unknown, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest<{ header(name: string): string | undefined }>(); + const value = request.header('x-tenant-id'); + return value && value.trim().length > 0 ? value.trim() : undefined; +}); diff --git a/api/src/dictionaries/dictionaries.controller.ts b/api/src/dictionaries/dictionaries.controller.ts new file mode 100644 index 0000000..a8bce13 --- /dev/null +++ b/api/src/dictionaries/dictionaries.controller.ts @@ -0,0 +1,66 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantId } from '../common/tenant-id.decorator'; +import { + CreateBlacklistDto, + CreateDrainageFieldDto, + CreatePhoneSegmentDto, + CreateSensitiveWordDto, + DictionariesService, +} from './dictionaries.service'; + +@ApiTags('dictionaries') +@Controller('admin/dictionaries') +export class DictionariesController { + constructor(private readonly dictionaries: DictionariesService) {} + + @Get('phone-segments') + listPhoneSegments() { + return this.dictionaries.listPhoneSegments(); + } + + @Post('phone-segments') + createPhoneSegment(@Body() body: CreatePhoneSegmentDto) { + return this.dictionaries.createPhoneSegment(body); + } + + @Get('sensitive-words') + listSensitiveWords() { + return this.dictionaries.listSensitiveWords(); + } + + @Post('sensitive-words') + createSensitiveWord(@Body() body: CreateSensitiveWordDto) { + return this.dictionaries.createSensitiveWord(body); + } + + @Get('blacklists/global') + listGlobalBlacklist() { + return this.dictionaries.listGlobalBlacklist(); + } + + @Post('blacklists/global') + createGlobalBlacklist(@Body() body: CreateBlacklistDto) { + return this.dictionaries.createGlobalBlacklist(body); + } + + @Get('blacklists/enterprise') + listEnterpriseBlacklist(@TenantId() tenantId?: string) { + return this.dictionaries.listEnterpriseBlacklist(tenantId); + } + + @Post('blacklists/enterprise') + createEnterpriseBlacklist(@Body() body: CreateBlacklistDto) { + return this.dictionaries.createEnterpriseBlacklist(body); + } + + @Get('drainage-fields') + listDrainageFields() { + return this.dictionaries.listDrainageFields(); + } + + @Post('drainage-fields') + createDrainageField(@Body() body: CreateDrainageFieldDto) { + return this.dictionaries.createDrainageField(body); + } +} diff --git a/api/src/dictionaries/dictionaries.module.ts b/api/src/dictionaries/dictionaries.module.ts new file mode 100644 index 0000000..5b7f6e0 --- /dev/null +++ b/api/src/dictionaries/dictionaries.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DictionariesController } from './dictionaries.controller'; +import { DictionariesService } from './dictionaries.service'; + +@Module({ + controllers: [DictionariesController], + providers: [DictionariesService], + exports: [DictionariesService], +}) +export class DictionariesModule {} diff --git a/api/src/dictionaries/dictionaries.service.ts b/api/src/dictionaries/dictionaries.service.ts new file mode 100644 index 0000000..8843973 --- /dev/null +++ b/api/src/dictionaries/dictionaries.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreatePhoneSegmentDto { + prefix: string; + carrier: string; + province?: string; + city?: string; +} + +export interface CreateSensitiveWordDto { + word: string; + level?: string; + status?: string; +} + +export interface CreateBlacklistDto { + tenantId?: string; + phoneNumber: string; + reason?: string; + status?: string; +} + +export interface CreateDrainageFieldDto { + code: string; + name: string; + fieldType: string; + required?: boolean; + status?: string; + description?: string; +} + +@Injectable() +export class DictionariesService { + constructor(private readonly prisma: PrismaService) {} + + listPhoneSegments() { + return this.prisma.phoneSegment.findMany({ orderBy: { prefix: 'asc' }, take: 200 }); + } + + createPhoneSegment(data: CreatePhoneSegmentDto) { + return this.prisma.phoneSegment.create({ data }); + } + + listSensitiveWords() { + return this.prisma.sensitiveWord.findMany({ orderBy: { createdAt: 'desc' }, take: 200 }); + } + + createSensitiveWord(data: CreateSensitiveWordDto) { + return this.prisma.sensitiveWord.create({ + data: { + word: data.word, + level: data.level ?? 'block', + status: data.status ?? 'active', + }, + }); + } + + listGlobalBlacklist() { + return this.prisma.globalBlacklist.findMany({ orderBy: { createdAt: 'desc' }, take: 200 }); + } + + createGlobalBlacklist(data: CreateBlacklistDto) { + return this.prisma.globalBlacklist.create({ + data: { + phoneNumber: data.phoneNumber, + reason: data.reason, + status: data.status ?? 'active', + }, + }); + } + + listEnterpriseBlacklist(tenantId?: string) { + return this.prisma.enterpriseBlacklist.findMany({ + where: tenantId ? { tenantId } : undefined, + orderBy: { createdAt: 'desc' }, + take: 200, + }); + } + + createEnterpriseBlacklist(data: CreateBlacklistDto) { + if (!data.tenantId) { + throw new Error('tenantId is required for enterprise blacklist'); + } + const createData: Prisma.EnterpriseBlacklistUncheckedCreateInput = { + tenantId: data.tenantId, + phoneNumber: data.phoneNumber, + reason: data.reason, + status: data.status ?? 'active', + }; + return this.prisma.enterpriseBlacklist.create({ data: createData }); + } + + listDrainageFields() { + return this.prisma.drainageField.findMany({ orderBy: { createdAt: 'desc' }, take: 200 }); + } + + createDrainageField(data: CreateDrainageFieldDto) { + return this.prisma.drainageField.create({ + data: { + code: data.code, + name: data.name, + fieldType: data.fieldType, + required: data.required ?? false, + status: data.status ?? 'active', + description: data.description, + }, + }); + } +} diff --git a/api/src/files/files.controller.ts b/api/src/files/files.controller.ts new file mode 100644 index 0000000..f27c44b --- /dev/null +++ b/api/src/files/files.controller.ts @@ -0,0 +1,25 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantId } from '../common/tenant-id.decorator'; +import { CreateFileObjectDto, CreatePresignedUploadDto, FilesService } from './files.service'; + +@ApiTags('files') +@Controller('admin/files') +export class FilesController { + constructor(private readonly files: FilesService) {} + + @Get() + list(@TenantId() tenantId?: string) { + return this.files.list(tenantId); + } + + @Post() + create(@Body() body: CreateFileObjectDto) { + return this.files.create(body); + } + + @Post('presigned-upload') + createPresignedUpload(@Body() body: CreatePresignedUploadDto) { + return this.files.createPresignedUpload(body); + } +} diff --git a/api/src/files/files.module.ts b/api/src/files/files.module.ts new file mode 100644 index 0000000..d7ef584 --- /dev/null +++ b/api/src/files/files.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { FilesController } from './files.controller'; +import { FilesService } from './files.service'; +import { ObjectStorageService } from './object-storage.service'; + +@Module({ + controllers: [FilesController], + providers: [FilesService, ObjectStorageService], + exports: [FilesService, ObjectStorageService], +}) +export class FilesModule {} diff --git a/api/src/files/files.service.ts b/api/src/files/files.service.ts new file mode 100644 index 0000000..17ed804 --- /dev/null +++ b/api/src/files/files.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { ObjectStorageService } from './object-storage.service'; + +export interface CreateFileObjectDto { + tenantId?: string; + bucket: string; + objectKey: string; + fileName: string; + contentType: string; + sizeBytes: number | string; + checksum?: string; + purpose: string; +} + +export interface CreatePresignedUploadDto { + objectKey: string; + expiresInSeconds?: number; +} + +@Injectable() +export class FilesService { + constructor( + private readonly prisma: PrismaService, + private readonly objectStorage: ObjectStorageService, + ) {} + + list(tenantId?: string) { + return this.prisma.fileObject.findMany({ + where: tenantId ? { tenantId } : undefined, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + create(data: CreateFileObjectDto) { + const createData: Prisma.FileObjectUncheckedCreateInput = { + tenantId: data.tenantId, + bucket: data.bucket, + objectKey: data.objectKey, + fileName: data.fileName, + contentType: data.contentType, + sizeBytes: BigInt(data.sizeBytes), + checksum: data.checksum, + purpose: data.purpose, + }; + return this.prisma.fileObject.create({ data: createData }); + } + + async createPresignedUpload(data: CreatePresignedUploadDto) { + const uploadUrl = await this.objectStorage.presignedPutObject(data.objectKey, data.expiresInSeconds ?? 3600); + return { + bucket: this.objectStorage.getBucket(), + objectKey: data.objectKey, + uploadUrl, + expiresInSeconds: data.expiresInSeconds ?? 3600, + }; + } +} diff --git a/api/src/files/object-storage.service.ts b/api/src/files/object-storage.service.ts new file mode 100644 index 0000000..0fd1bcb --- /dev/null +++ b/api/src/files/object-storage.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Client } from 'minio'; + +@Injectable() +export class ObjectStorageService { + private readonly client: Client; + private readonly bucket: string; + + constructor(config: ConfigService) { + const endpoint = config.get('MINIO_ENDPOINT') ?? 'localhost:9000'; + const [endPoint, portText] = endpoint.split(':'); + this.bucket = config.get('MINIO_BUCKET') ?? 'cmpp-platform'; + this.client = new Client({ + endPoint, + port: Number(portText ?? 9000), + useSSL: config.get('MINIO_USE_SSL') === 'true', + accessKey: config.get('MINIO_ACCESS_KEY') ?? 'cmpp_minio', + secretKey: config.get('MINIO_SECRET_KEY') ?? 'cmpp_minio_password', + }); + } + + presignedPutObject(objectKey: string, expirySeconds = 3600) { + return this.client.presignedPutObject(this.bucket, objectKey, expirySeconds); + } + + getBucket() { + return this.bucket; + } +} diff --git a/api/src/health.controller.ts b/api/src/health.controller.ts new file mode 100644 index 0000000..e8d621d --- /dev/null +++ b/api/src/health.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('health') +@Controller('health') +export class HealthController { + @Get() + getHealth() { + return { + status: 'ok', + service: 'cmpp-platform-api', + timestamp: new Date().toISOString(), + }; + } +} diff --git a/api/src/main.ts b/api/src/main.ts new file mode 100644 index 0000000..4d481e3 --- /dev/null +++ b/api/src/main.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('api'); + + const swaggerConfig = new DocumentBuilder() + .setTitle('CMPP Platform API') + .setDescription('First-version CMPP SMS platform API') + .setVersion('0.1.0') + .build(); + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api/docs', app, document); + + const port = Number(process.env.API_PORT ?? 3000); + await app.listen(port); +} + +void bootstrap(); diff --git a/api/src/prisma/prisma.module.ts b/api/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/api/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/api/src/prisma/prisma.service.ts b/api/src/prisma/prisma.service.ts new file mode 100644 index 0000000..9337abf --- /dev/null +++ b/api/src/prisma/prisma.service.ts @@ -0,0 +1,19 @@ +import { Injectable, OnModuleDestroy } from '@nestjs/common'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleDestroy { + constructor() { + super({ + adapter: new PrismaPg( + process.env.DATABASE_URL ?? + 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', + ), + }); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/api/src/sms-config/admin-sms-config.controller.ts b/api/src/sms-config/admin-sms-config.controller.ts new file mode 100644 index 0000000..9368641 --- /dev/null +++ b/api/src/sms-config/admin-sms-config.controller.ts @@ -0,0 +1,49 @@ +import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ReviewDto, SmsConfigService } from './sms-config.service'; + +@ApiTags('admin-sms-config') +@Controller('admin') +export class AdminSmsConfigController { + constructor(private readonly smsConfig: SmsConfigService) {} + + @Get('enterprise-applications') + listApplications(@Query('tenantId') tenantId?: string) { + return this.smsConfig.listApplications(tenantId); + } + + @Get('enterprise-signatures') + listSignatures(@Query('tenantId') tenantId?: string) { + return this.smsConfig.listSignatures(tenantId); + } + + @Get('enterprise-templates') + listTemplates(@Query('tenantId') tenantId?: string) { + return this.smsConfig.listTemplates(tenantId); + } + + @Get('audit-records') + listAuditRecords(@Query('targetType') targetType?: string, @Query('targetId') targetId?: string) { + return this.smsConfig.listAuditRecords(targetType, targetId); + } + + @Post('signatures/:id/approve') + approveSignature(@Param('id') signatureId: string, @Body() body: ReviewDto) { + return this.smsConfig.approveSignature(signatureId, body); + } + + @Post('signatures/:id/reject') + rejectSignature(@Param('id') signatureId: string, @Body() body: ReviewDto) { + return this.smsConfig.rejectSignature(signatureId, body); + } + + @Post('templates/:id/approve') + approveTemplate(@Param('id') templateId: string, @Body() body: ReviewDto) { + return this.smsConfig.approveTemplate(templateId, body); + } + + @Post('templates/:id/reject') + rejectTemplate(@Param('id') templateId: string, @Body() body: ReviewDto) { + return this.smsConfig.rejectTemplate(templateId, body); + } +} diff --git a/api/src/sms-config/client-sms-config.controller.ts b/api/src/sms-config/client-sms-config.controller.ts new file mode 100644 index 0000000..c358b08 --- /dev/null +++ b/api/src/sms-config/client-sms-config.controller.ts @@ -0,0 +1,61 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantId } from '../common/tenant-id.decorator'; +import { + CreateSignatureMaterialDto, + CreateSmsApplicationDto, + CreateSmsSignatureDto, + CreateSmsTemplateDto, + SmsConfigService, +} from './sms-config.service'; + +@ApiTags('client-sms-config') +@Controller('client') +export class ClientSmsConfigController { + constructor(private readonly smsConfig: SmsConfigService) {} + + @Get('applications') + listApplications(@TenantId() tenantId?: string) { + return this.smsConfig.listApplications(tenantId); + } + + @Post('applications') + createApplication(@Body() body: CreateSmsApplicationDto) { + return this.smsConfig.createApplication(body); + } + + @Get('signatures') + listSignatures(@TenantId() tenantId?: string) { + return this.smsConfig.listSignatures(tenantId); + } + + @Post('signatures') + createSignature(@Body() body: CreateSmsSignatureDto) { + return this.smsConfig.createSignature(body); + } + + @Post('signatures/:id/materials') + createSignatureMaterial(@Param('id') signatureId: string, @Body() body: Omit) { + return this.smsConfig.createSignatureMaterial({ ...body, signatureId }); + } + + @Post('signatures/:id/submit') + submitSignature(@Param('id') signatureId: string) { + return this.smsConfig.submitSignature(signatureId); + } + + @Get('templates') + listTemplates(@TenantId() tenantId?: string) { + return this.smsConfig.listTemplates(tenantId); + } + + @Post('templates') + createTemplate(@Body() body: CreateSmsTemplateDto) { + return this.smsConfig.createTemplate(body); + } + + @Post('templates/:id/submit') + submitTemplate(@Param('id') templateId: string) { + return this.smsConfig.submitTemplate(templateId); + } +} diff --git a/api/src/sms-config/sms-config.module.ts b/api/src/sms-config/sms-config.module.ts new file mode 100644 index 0000000..bd6d349 --- /dev/null +++ b/api/src/sms-config/sms-config.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AdminSmsConfigController } from './admin-sms-config.controller'; +import { ClientSmsConfigController } from './client-sms-config.controller'; +import { SmsConfigService } from './sms-config.service'; + +@Module({ + controllers: [ClientSmsConfigController, AdminSmsConfigController], + providers: [SmsConfigService], + exports: [SmsConfigService], +}) +export class SmsConfigModule {} diff --git a/api/src/sms-config/sms-config.service.ts b/api/src/sms-config/sms-config.service.ts new file mode 100644 index 0000000..a8edaeb --- /dev/null +++ b/api/src/sms-config/sms-config.service.ts @@ -0,0 +1,292 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { randomBytes, createHash } from 'node:crypto'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreateSmsApplicationDto { + tenantId: string; + name: string; + scene?: string; + callbackUrl?: string; + dailyLimit?: number; + maxPhonesPerTask?: number; + templateMismatchMode?: string; + ipAllowlist?: string[]; +} + +export interface CreateSmsSignatureDto { + tenantId: string; + applicationId?: string; + name: string; + purpose?: string; + drainageInfo?: Record; +} + +export interface CreateSignatureMaterialDto { + signatureId: string; + fileObjectId?: string; + materialType: string; + title: string; + description?: string; +} + +export interface CreateSmsTemplateDto { + tenantId: string; + applicationId: string; + signatureId?: string; + name: string; + content: string; + category?: string; + variables?: Array<{ name: string; example?: string; required?: boolean }>; +} + +export interface ReviewDto { + reviewerId?: string; + reason?: string; +} + +@Injectable() +export class SmsConfigService { + constructor(private readonly prisma: PrismaService) {} + + listApplications(tenantId?: string) { + return this.prisma.smsApplication.findMany({ + where: tenantId ? { tenantId } : undefined, + include: { ipAllowlist: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createApplication(data: CreateSmsApplicationDto) { + const secret = randomBytes(24).toString('hex'); + return this.prisma.smsApplication.create({ + data: { + tenantId: data.tenantId, + name: data.name, + scene: data.scene, + callbackUrl: data.callbackUrl, + secretHash: hashSecret(secret), + dailyLimit: data.dailyLimit, + maxPhonesPerTask: data.maxPhonesPerTask ?? 1000000, + templateMismatchMode: data.templateMismatchMode ?? 'reject', + ipAllowlist: { + create: (data.ipAllowlist ?? []).map((ipCidr) => ({ ipCidr })), + }, + }, + include: { ipAllowlist: true }, + }); + } + + listSignatures(tenantId?: string) { + return this.prisma.smsSignature.findMany({ + where: tenantId ? { tenantId } : undefined, + include: { materials: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createSignature(data: CreateSmsSignatureDto) { + return this.prisma.smsSignature.create({ + data: { + tenantId: data.tenantId, + applicationId: data.applicationId, + name: data.name, + purpose: data.purpose, + drainageInfo: data.drainageInfo as Prisma.InputJsonValue | undefined, + }, + }); + } + + createSignatureMaterial(data: CreateSignatureMaterialDto) { + return this.prisma.signatureMaterial.create({ + data: { + signatureId: data.signatureId, + fileObjectId: data.fileObjectId, + materialType: data.materialType, + title: data.title, + description: data.description, + }, + }); + } + + async submitSignature(signatureId: string) { + const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } }); + if (!signature) { + throw new NotFoundException('Signature not found'); + } + + const updated = await this.prisma.smsSignature.update({ + where: { id: signatureId }, + data: { auditStatus: 'pending', rejectReason: null }, + }); + await this.createAuditRecord({ + tenantId: signature.tenantId, + targetType: 'sms_signature', + targetId: signatureId, + action: 'submit', + statusBefore: signature.auditStatus, + statusAfter: 'pending', + }); + return updated; + } + + listTemplates(tenantId?: string) { + return this.prisma.smsTemplate.findMany({ + where: tenantId ? { tenantId } : undefined, + include: { variables: true }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createTemplate(data: CreateSmsTemplateDto) { + return this.prisma.smsTemplate.create({ + data: { + tenantId: data.tenantId, + applicationId: data.applicationId, + signatureId: data.signatureId, + name: data.name, + content: data.content, + category: data.category, + billingUnits: estimateBillingUnits(data.content), + variables: { + create: (data.variables ?? inferTemplateVariables(data.content)).map((variable: TemplateVariableInput) => ({ + name: variable.name, + example: variable.example, + required: variable.required ?? true, + })), + }, + }, + include: { variables: true }, + }); + } + + async submitTemplate(templateId: string) { + const template = await this.prisma.smsTemplate.findUnique({ where: { id: templateId } }); + if (!template) { + throw new NotFoundException('Template not found'); + } + + const updated = await this.prisma.smsTemplate.update({ + where: { id: templateId }, + data: { auditStatus: 'pending', rejectReason: null }, + }); + await this.createAuditRecord({ + tenantId: template.tenantId, + targetType: 'sms_template', + targetId: templateId, + action: 'submit', + statusBefore: template.auditStatus, + statusAfter: 'pending', + }); + return updated; + } + + listAuditRecords(targetType?: string, targetId?: string) { + return this.prisma.auditRecord.findMany({ + where: { + targetType, + targetId, + }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + approveSignature(signatureId: string, data: ReviewDto) { + return this.reviewSignature(signatureId, 'approved', 'approve', data); + } + + rejectSignature(signatureId: string, data: ReviewDto) { + return this.reviewSignature(signatureId, 'rejected', 'reject', data); + } + + approveTemplate(templateId: string, data: ReviewDto) { + return this.reviewTemplate(templateId, 'approved', 'approve', data); + } + + rejectTemplate(templateId: string, data: ReviewDto) { + return this.reviewTemplate(templateId, 'rejected', 'reject', data); + } + + private async reviewSignature(signatureId: string, statusAfter: string, action: string, data: ReviewDto) { + const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } }); + if (!signature) { + throw new NotFoundException('Signature not found'); + } + + const updated = await this.prisma.smsSignature.update({ + where: { id: signatureId }, + data: { + auditStatus: statusAfter, + rejectReason: statusAfter === 'rejected' ? data.reason : null, + }, + }); + await this.createAuditRecord({ + tenantId: signature.tenantId, + targetType: 'sms_signature', + targetId: signatureId, + action, + statusBefore: signature.auditStatus, + statusAfter, + reason: data.reason, + reviewerId: data.reviewerId, + }); + return updated; + } + + private async reviewTemplate(templateId: string, statusAfter: string, action: string, data: ReviewDto) { + const template = await this.prisma.smsTemplate.findUnique({ where: { id: templateId } }); + if (!template) { + throw new NotFoundException('Template not found'); + } + + const updated = await this.prisma.smsTemplate.update({ + where: { id: templateId }, + data: { + auditStatus: statusAfter, + rejectReason: statusAfter === 'rejected' ? data.reason : null, + }, + }); + await this.createAuditRecord({ + tenantId: template.tenantId, + targetType: 'sms_template', + targetId: templateId, + action, + statusBefore: template.auditStatus, + statusAfter, + reason: data.reason, + reviewerId: data.reviewerId, + }); + return updated; + } + + private createAuditRecord(data: Prisma.AuditRecordUncheckedCreateInput) { + return this.prisma.auditRecord.create({ data }); + } +} + +interface TemplateVariableInput { + name: string; + example?: string; + required?: boolean; +} + +function hashSecret(secret: string) { + return createHash('sha256').update(secret).digest('hex'); +} + +function estimateBillingUnits(content: string) { + const length = [...content].length; + if (length <= 70) { + return 1; + } + return Math.ceil(length / 67); +} + +function inferTemplateVariables(content: string): TemplateVariableInput[] { + const matches = content.match(/\$\{[a-zA-Z0-9_]+\}/g) ?? []; + return [...new Set(matches)].map((match) => ({ name: match.slice(2, -1), required: true })); +} diff --git a/api/src/spike/bullmq-link-spike.mjs b/api/src/spike/bullmq-link-spike.mjs new file mode 100644 index 0000000..14c141a --- /dev/null +++ b/api/src/spike/bullmq-link-spike.mjs @@ -0,0 +1,215 @@ +import { Queue, Worker } from 'bullmq'; +import IORedis from 'ioredis'; +import { randomUUID } from 'node:crypto'; +import { performance } from 'node:perf_hooks'; + +const connection = new IORedis({ + host: process.env.REDIS_HOST ?? '127.0.0.1', + port: Number(process.env.REDIS_PORT ?? 6379), + maxRetriesPerRequest: null, +}); + +const submitQueueName = 'cmpp.submit.commands'; +const submitResultQueueName = 'cmpp.submit.results'; +const receiptQueueName = 'cmpp.receipt.events'; + +const submitQueue = new Queue(submitQueueName, { connection }); +const submitResultQueue = new Queue(submitResultQueueName, { connection }); +const receiptQueue = new Queue(receiptQueueName, { connection }); + +const messageCount = Number(process.env.SPIKE_MESSAGE_COUNT ?? 15000); +const concurrency = Number(process.env.SPIKE_CONCURRENCY ?? 500); + +function createSubmitCommand(index) { + const padded = String(index).padStart(6, '0'); + const now = new Date().toISOString(); + return { + schemaVersion: 'v1', + messageType: 'SubmitCommand', + traceId: `trace-bullmq-${padded}`, + messageId: `msg-bullmq-${padded}`, + channelId: 'sms-channel-cmpp-spike', + createdAt: now, + tenantId: 'tenant-spike', + applicationId: 'app-spike', + taskId: 'task-bullmq-spike', + submitId: `submit-bullmq-${padded}`, + phoneNumber: '13800138000', + content: '您的验证码为 123456,5 分钟内有效。', + signature: '测试平台', + templateId: 'tpl-spike', + billingUnits: 1, + route: { + channelCode: 'CMCC-CMPP-SPIKE', + cmppAccountCode: 'cmpp-account-spike', + priority: 10, + rateLimitPerSecond: 500, + }, + cmpp: { + serviceId: 'CMPP', + srcId: '106900000000', + registeredDelivery: 1, + msgFmt: 15, + feeUserType: 2, + feeCode: '0', + feeType: '01', + }, + retry: { + attempt: 0, + maxAttempts: 3, + }, + }; +} + +async function cleanQueues() { + for (const queue of [submitQueue, submitResultQueue, receiptQueue]) { + await queue.drain(true); + await queue.obliterate({ force: true }); + } +} + +async function closeAll(workers) { + await Promise.all(workers.map((worker) => worker.close())); + await Promise.all([submitQueue.close(), submitResultQueue.close(), receiptQueue.close()]); + await connection.quit(); +} + +let submitResults = 0; +let receiptEvents = 0; + +const gatewayWorker = new Worker( + submitQueueName, + async (job) => { + const cmd = job.data; + const sequenceId = Number(job.id.replace(/\D/g, '').slice(-9)) || job.attemptsMade + 1; + const gatewayMessageId = `gw-${cmd.messageId}`; + const now = new Date().toISOString(); + + await submitResultQueue.add( + 'SubmitResult', + { + schemaVersion: 'v1', + messageType: 'SubmitResult', + traceId: cmd.traceId, + messageId: cmd.messageId, + channelId: cmd.channelId, + createdAt: now, + sequenceId, + gatewayMessageId, + submitStatus: 'accepted', + submittedAt: now, + }, + { jobId: `submit-result-${cmd.messageId}` }, + ); + + await receiptQueue.add( + 'ReceiptEvent', + { + schemaVersion: 'v1', + messageType: 'ReceiptEvent', + traceId: cmd.traceId, + messageId: cmd.messageId, + channelId: cmd.channelId, + createdAt: now, + sequenceId, + gatewayMessageId, + receiptStatus: 'delivered', + rawStatus: 'DELIVRD', + deliveredAt: now, + }, + { jobId: `receipt-${cmd.messageId}` }, + ); + }, + { connection, concurrency }, +); + +const submitResultWorker = new Worker( + submitResultQueueName, + async () => { + submitResults += 1; + }, + { connection, concurrency }, +); + +const receiptWorker = new Worker( + receiptQueueName, + async () => { + receiptEvents += 1; + }, + { connection, concurrency }, +); + +async function waitForCompletion() { + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + if (submitResults >= messageCount && receiptEvents >= messageCount) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 10)); + } + throw new Error(`Timed out waiting for BullMQ events: submitResults=${submitResults} receiptEvents=${receiptEvents}`); +} + +async function main() { + await connection.ping(); + await cleanQueues(); + + const workers = [gatewayWorker, submitResultWorker, receiptWorker]; + const runId = randomUUID(); + const startedAt = performance.now(); + + for (let offset = 0; offset < messageCount; offset += 1000) { + const jobs = []; + for (let index = offset; index < Math.min(offset + 1000, messageCount); index += 1) { + const cmd = createSubmitCommand(index); + jobs.push({ + name: 'SubmitCommand', + data: cmd, + opts: { + jobId: `${runId}-${cmd.messageId}`, + attempts: 3, + }, + }); + } + await submitQueue.addBulk(jobs); + } + + const enqueuedAt = performance.now(); + await waitForCompletion(); + const completedAt = performance.now(); + + const enqueueDurationMs = enqueuedAt - startedAt; + const totalDurationMs = completedAt - startedAt; + const enqueueTps = messageCount / (enqueueDurationMs / 1000); + const endToEndTps = messageCount / (totalDurationMs / 1000); + + const meets500Tps = enqueueTps >= 500 && endToEndTps >= 500; + console.log( + JSON.stringify( + { + messageCount, + concurrency, + submitResults, + receiptEvents, + enqueueDurationMs: Number(enqueueDurationMs.toFixed(2)), + totalDurationMs: Number(totalDurationMs.toFixed(2)), + enqueueTps: Number(enqueueTps.toFixed(2)), + endToEndTps: Number(endToEndTps.toFixed(2)), + meets500Tps, + }, + null, + 2, + ), + ); + + await closeAll(workers); + if (!meets500Tps) { + process.exitCode = 1; + } +} + +main().catch(async (error) => { + console.error(error); + await closeAll([gatewayWorker, submitResultWorker, receiptWorker]); + process.exitCode = 1; +}); diff --git a/api/src/tenants/tenants.controller.ts b/api/src/tenants/tenants.controller.ts new file mode 100644 index 0000000..88477b6 --- /dev/null +++ b/api/src/tenants/tenants.controller.ts @@ -0,0 +1,24 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { CreateTenantDto, TenantsService } from './tenants.service'; + +@ApiTags('tenants') +@Controller('admin/tenants') +export class TenantsController { + constructor(private readonly tenants: TenantsService) {} + + @Get() + list() { + return this.tenants.list(); + } + + @Get(':id') + get(@Param('id') id: string) { + return this.tenants.get(id); + } + + @Post() + create(@Body() body: CreateTenantDto) { + return this.tenants.create(body); + } +} diff --git a/api/src/tenants/tenants.module.ts b/api/src/tenants/tenants.module.ts new file mode 100644 index 0000000..522f740 --- /dev/null +++ b/api/src/tenants/tenants.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TenantsController } from './tenants.controller'; +import { TenantsService } from './tenants.service'; + +@Module({ + controllers: [TenantsController], + providers: [TenantsService], + exports: [TenantsService], +}) +export class TenantsModule {} diff --git a/api/src/tenants/tenants.service.ts b/api/src/tenants/tenants.service.ts new file mode 100644 index 0000000..bd4ff0c --- /dev/null +++ b/api/src/tenants/tenants.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreateTenantDto { + name: string; + code: string; + status?: string; +} + +@Injectable() +export class TenantsService { + constructor(private readonly prisma: PrismaService) {} + + list() { + return this.prisma.tenant.findMany({ + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + get(id: string) { + return this.prisma.tenant.findUnique({ where: { id } }); + } + + create(data: CreateTenantDto) { + return this.prisma.tenant.create({ + data: { + name: data.name, + code: data.code, + status: data.status ?? 'active', + }, + }); + } +} diff --git a/api/src/users/users.controller.ts b/api/src/users/users.controller.ts new file mode 100644 index 0000000..8af0da2 --- /dev/null +++ b/api/src/users/users.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantId } from '../common/tenant-id.decorator'; +import { + AssignPermissionDto, + AssignRoleDto, + CreatePermissionDto, + CreateRoleDto, + CreateUserDto, + UsersService, +} from './users.service'; + +@ApiTags('users') +@Controller('admin/users') +export class UsersController { + constructor(private readonly users: UsersService) {} + + @Get() + list(@TenantId() tenantId?: string) { + return this.users.list(tenantId); + } + + @Post() + create(@Body() body: CreateUserDto) { + return this.users.create(body); + } + + @Get('roles') + listRoles() { + return this.users.listRoles(); + } + + @Post('roles') + createRole(@Body() body: CreateRoleDto) { + return this.users.createRole(body); + } + + @Get('permissions') + listPermissions() { + return this.users.listPermissions(); + } + + @Post('permissions') + createPermission(@Body() body: CreatePermissionDto) { + return this.users.createPermission(body); + } + + @Post('roles/assign') + assignRole(@Body() body: AssignRoleDto) { + return this.users.assignRole(body); + } + + @Post('permissions/assign') + assignPermission(@Body() body: AssignPermissionDto) { + return this.users.assignPermission(body); + } +} diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts new file mode 100644 index 0000000..513776d --- /dev/null +++ b/api/src/users/users.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +@Module({ + controllers: [UsersController], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts new file mode 100644 index 0000000..235ae95 --- /dev/null +++ b/api/src/users/users.service.ts @@ -0,0 +1,111 @@ +import { createHash } from 'node:crypto'; +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +export interface CreateUserDto { + tenantId?: string; + username: string; + displayName: string; + password: string; + status?: string; +} + +export interface CreateRoleDto { + code: string; + name: string; + scope?: string; + description?: string; +} + +export interface CreatePermissionDto { + code: string; + name: string; + description?: string; +} + +export interface AssignRoleDto { + userId: string; + roleId: string; +} + +export interface AssignPermissionDto { + roleId: string; + permissionId: string; +} + +@Injectable() +export class UsersService { + constructor(private readonly prisma: PrismaService) {} + + list(tenantId?: string) { + return this.prisma.user.findMany({ + where: tenantId ? { tenantId } : undefined, + include: { roles: { include: { role: true } } }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + findByUsername(username: string) { + return this.prisma.user.findUnique({ where: { username } }); + } + + create(data: CreateUserDto) { + return this.prisma.user.create({ + data: { + tenantId: data.tenantId, + username: data.username, + displayName: data.displayName, + passwordHash: hashPassword(data.password), + status: data.status ?? 'active', + }, + }); + } + + listRoles() { + return this.prisma.role.findMany({ + include: { permissions: { include: { permission: true } } }, + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + createRole(data: CreateRoleDto) { + return this.prisma.role.create({ + data: { + code: data.code, + name: data.name, + scope: data.scope ?? 'platform', + description: data.description, + }, + }); + } + + listPermissions() { + return this.prisma.permission.findMany({ orderBy: { createdAt: 'desc' }, take: 200 }); + } + + createPermission(data: CreatePermissionDto) { + return this.prisma.permission.create({ data }); + } + + assignRole(data: AssignRoleDto) { + return this.prisma.userRole.upsert({ + where: { userId_roleId: { userId: data.userId, roleId: data.roleId } }, + update: {}, + create: data, + }); + } + + assignPermission(data: AssignPermissionDto) { + return this.prisma.rolePermission.upsert({ + where: { roleId_permissionId: { roleId: data.roleId, permissionId: data.permissionId } }, + update: {}, + create: data, + }); + } +} + +export function hashPassword(password: string) { + return createHash('sha256').update(password).digest('hex'); +} diff --git a/api/tsconfig.build.json b/api/tsconfig.build.json new file mode 100644 index 0000000..53dc684 --- /dev/null +++ b/api/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false + }, + "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"] +} diff --git a/api/tsconfig.build.tsbuildinfo b/api/tsconfig.build.tsbuildinfo new file mode 100644 index 0000000..959c66f --- /dev/null +++ b/api/tsconfig.build.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.dom.asynciterable.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2025.float16.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/typescript/lib/lib.es2022.full.d.ts","./node_modules/reflect-metadata/index.d.ts","./node_modules/@nestjs/common/decorators/core/bind.decorator.d.ts","./node_modules/@nestjs/common/interfaces/abstract.interface.d.ts","./node_modules/@nestjs/common/interfaces/controllers/controller-metadata.interface.d.ts","./node_modules/@nestjs/common/interfaces/controllers/controller.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/arguments-host.interface.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/exception-filter.interface.d.ts","./node_modules/rxjs/dist/types/internal/subscription.d.ts","./node_modules/rxjs/dist/types/internal/subscriber.d.ts","./node_modules/rxjs/dist/types/internal/operator.d.ts","./node_modules/rxjs/dist/types/internal/observable.d.ts","./node_modules/rxjs/dist/types/internal/types.d.ts","./node_modules/rxjs/dist/types/internal/operators/audit.d.ts","./node_modules/rxjs/dist/types/internal/operators/audittime.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffer.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffercount.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffertime.d.ts","./node_modules/rxjs/dist/types/internal/operators/buffertoggle.d.ts","./node_modules/rxjs/dist/types/internal/operators/bufferwhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/catcherror.d.ts","./node_modules/rxjs/dist/types/internal/operators/combinelatestall.d.ts","./node_modules/rxjs/dist/types/internal/operators/combineall.d.ts","./node_modules/rxjs/dist/types/internal/operators/combinelatest.d.ts","./node_modules/rxjs/dist/types/internal/operators/combinelatestwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/concat.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatall.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatmapto.d.ts","./node_modules/rxjs/dist/types/internal/operators/concatwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/connect.d.ts","./node_modules/rxjs/dist/types/internal/operators/count.d.ts","./node_modules/rxjs/dist/types/internal/operators/debounce.d.ts","./node_modules/rxjs/dist/types/internal/operators/debouncetime.d.ts","./node_modules/rxjs/dist/types/internal/operators/defaultifempty.d.ts","./node_modules/rxjs/dist/types/internal/operators/delay.d.ts","./node_modules/rxjs/dist/types/internal/operators/delaywhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/dematerialize.d.ts","./node_modules/rxjs/dist/types/internal/operators/distinct.d.ts","./node_modules/rxjs/dist/types/internal/operators/distinctuntilchanged.d.ts","./node_modules/rxjs/dist/types/internal/operators/distinctuntilkeychanged.d.ts","./node_modules/rxjs/dist/types/internal/operators/elementat.d.ts","./node_modules/rxjs/dist/types/internal/operators/endwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/every.d.ts","./node_modules/rxjs/dist/types/internal/operators/exhaustall.d.ts","./node_modules/rxjs/dist/types/internal/operators/exhaust.d.ts","./node_modules/rxjs/dist/types/internal/operators/exhaustmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/expand.d.ts","./node_modules/rxjs/dist/types/internal/operators/filter.d.ts","./node_modules/rxjs/dist/types/internal/operators/finalize.d.ts","./node_modules/rxjs/dist/types/internal/operators/find.d.ts","./node_modules/rxjs/dist/types/internal/operators/findindex.d.ts","./node_modules/rxjs/dist/types/internal/operators/first.d.ts","./node_modules/rxjs/dist/types/internal/subject.d.ts","./node_modules/rxjs/dist/types/internal/operators/groupby.d.ts","./node_modules/rxjs/dist/types/internal/operators/ignoreelements.d.ts","./node_modules/rxjs/dist/types/internal/operators/isempty.d.ts","./node_modules/rxjs/dist/types/internal/operators/last.d.ts","./node_modules/rxjs/dist/types/internal/operators/map.d.ts","./node_modules/rxjs/dist/types/internal/operators/mapto.d.ts","./node_modules/rxjs/dist/types/internal/notification.d.ts","./node_modules/rxjs/dist/types/internal/operators/materialize.d.ts","./node_modules/rxjs/dist/types/internal/operators/max.d.ts","./node_modules/rxjs/dist/types/internal/operators/merge.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergeall.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergemap.d.ts","./node_modules/rxjs/dist/types/internal/operators/flatmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergemapto.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergescan.d.ts","./node_modules/rxjs/dist/types/internal/operators/mergewith.d.ts","./node_modules/rxjs/dist/types/internal/operators/min.d.ts","./node_modules/rxjs/dist/types/internal/observable/connectableobservable.d.ts","./node_modules/rxjs/dist/types/internal/operators/multicast.d.ts","./node_modules/rxjs/dist/types/internal/operators/observeon.d.ts","./node_modules/rxjs/dist/types/internal/operators/onerrorresumenextwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/pairwise.d.ts","./node_modules/rxjs/dist/types/internal/operators/partition.d.ts","./node_modules/rxjs/dist/types/internal/operators/pluck.d.ts","./node_modules/rxjs/dist/types/internal/operators/publish.d.ts","./node_modules/rxjs/dist/types/internal/operators/publishbehavior.d.ts","./node_modules/rxjs/dist/types/internal/operators/publishlast.d.ts","./node_modules/rxjs/dist/types/internal/operators/publishreplay.d.ts","./node_modules/rxjs/dist/types/internal/operators/race.d.ts","./node_modules/rxjs/dist/types/internal/operators/racewith.d.ts","./node_modules/rxjs/dist/types/internal/operators/reduce.d.ts","./node_modules/rxjs/dist/types/internal/operators/repeat.d.ts","./node_modules/rxjs/dist/types/internal/operators/repeatwhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/retry.d.ts","./node_modules/rxjs/dist/types/internal/operators/retrywhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/refcount.d.ts","./node_modules/rxjs/dist/types/internal/operators/sample.d.ts","./node_modules/rxjs/dist/types/internal/operators/sampletime.d.ts","./node_modules/rxjs/dist/types/internal/operators/scan.d.ts","./node_modules/rxjs/dist/types/internal/operators/sequenceequal.d.ts","./node_modules/rxjs/dist/types/internal/operators/share.d.ts","./node_modules/rxjs/dist/types/internal/operators/sharereplay.d.ts","./node_modules/rxjs/dist/types/internal/operators/single.d.ts","./node_modules/rxjs/dist/types/internal/operators/skip.d.ts","./node_modules/rxjs/dist/types/internal/operators/skiplast.d.ts","./node_modules/rxjs/dist/types/internal/operators/skipuntil.d.ts","./node_modules/rxjs/dist/types/internal/operators/skipwhile.d.ts","./node_modules/rxjs/dist/types/internal/operators/startwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/subscribeon.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchall.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchmap.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchmapto.d.ts","./node_modules/rxjs/dist/types/internal/operators/switchscan.d.ts","./node_modules/rxjs/dist/types/internal/operators/take.d.ts","./node_modules/rxjs/dist/types/internal/operators/takelast.d.ts","./node_modules/rxjs/dist/types/internal/operators/takeuntil.d.ts","./node_modules/rxjs/dist/types/internal/operators/takewhile.d.ts","./node_modules/rxjs/dist/types/internal/operators/tap.d.ts","./node_modules/rxjs/dist/types/internal/operators/throttle.d.ts","./node_modules/rxjs/dist/types/internal/operators/throttletime.d.ts","./node_modules/rxjs/dist/types/internal/operators/throwifempty.d.ts","./node_modules/rxjs/dist/types/internal/operators/timeinterval.d.ts","./node_modules/rxjs/dist/types/internal/operators/timeout.d.ts","./node_modules/rxjs/dist/types/internal/operators/timeoutwith.d.ts","./node_modules/rxjs/dist/types/internal/operators/timestamp.d.ts","./node_modules/rxjs/dist/types/internal/operators/toarray.d.ts","./node_modules/rxjs/dist/types/internal/operators/window.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowcount.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowtime.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowtoggle.d.ts","./node_modules/rxjs/dist/types/internal/operators/windowwhen.d.ts","./node_modules/rxjs/dist/types/internal/operators/withlatestfrom.d.ts","./node_modules/rxjs/dist/types/internal/operators/zip.d.ts","./node_modules/rxjs/dist/types/internal/operators/zipall.d.ts","./node_modules/rxjs/dist/types/internal/operators/zipwith.d.ts","./node_modules/rxjs/dist/types/operators/index.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/action.d.ts","./node_modules/rxjs/dist/types/internal/scheduler.d.ts","./node_modules/rxjs/dist/types/internal/testing/testmessage.d.ts","./node_modules/rxjs/dist/types/internal/testing/subscriptionlog.d.ts","./node_modules/rxjs/dist/types/internal/testing/subscriptionloggable.d.ts","./node_modules/rxjs/dist/types/internal/testing/coldobservable.d.ts","./node_modules/rxjs/dist/types/internal/testing/hotobservable.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asyncscheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/timerhandle.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asyncaction.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/virtualtimescheduler.d.ts","./node_modules/rxjs/dist/types/internal/testing/testscheduler.d.ts","./node_modules/rxjs/dist/types/testing/index.d.ts","./node_modules/rxjs/dist/types/internal/symbol/observable.d.ts","./node_modules/rxjs/dist/types/internal/observable/dom/animationframes.d.ts","./node_modules/rxjs/dist/types/internal/behaviorsubject.d.ts","./node_modules/rxjs/dist/types/internal/replaysubject.d.ts","./node_modules/rxjs/dist/types/internal/asyncsubject.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asapscheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/asap.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/async.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/queuescheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/queue.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/animationframescheduler.d.ts","./node_modules/rxjs/dist/types/internal/scheduler/animationframe.d.ts","./node_modules/rxjs/dist/types/internal/util/identity.d.ts","./node_modules/rxjs/dist/types/internal/util/pipe.d.ts","./node_modules/rxjs/dist/types/internal/util/noop.d.ts","./node_modules/rxjs/dist/types/internal/util/isobservable.d.ts","./node_modules/rxjs/dist/types/internal/lastvaluefrom.d.ts","./node_modules/rxjs/dist/types/internal/firstvaluefrom.d.ts","./node_modules/rxjs/dist/types/internal/util/argumentoutofrangeerror.d.ts","./node_modules/rxjs/dist/types/internal/util/emptyerror.d.ts","./node_modules/rxjs/dist/types/internal/util/notfounderror.d.ts","./node_modules/rxjs/dist/types/internal/util/objectunsubscribederror.d.ts","./node_modules/rxjs/dist/types/internal/util/sequenceerror.d.ts","./node_modules/rxjs/dist/types/internal/util/unsubscriptionerror.d.ts","./node_modules/rxjs/dist/types/internal/observable/bindcallback.d.ts","./node_modules/rxjs/dist/types/internal/observable/bindnodecallback.d.ts","./node_modules/rxjs/dist/types/internal/anycatcher.d.ts","./node_modules/rxjs/dist/types/internal/observable/combinelatest.d.ts","./node_modules/rxjs/dist/types/internal/observable/concat.d.ts","./node_modules/rxjs/dist/types/internal/observable/connectable.d.ts","./node_modules/rxjs/dist/types/internal/observable/defer.d.ts","./node_modules/rxjs/dist/types/internal/observable/empty.d.ts","./node_modules/rxjs/dist/types/internal/observable/forkjoin.d.ts","./node_modules/rxjs/dist/types/internal/observable/from.d.ts","./node_modules/rxjs/dist/types/internal/observable/fromevent.d.ts","./node_modules/rxjs/dist/types/internal/observable/fromeventpattern.d.ts","./node_modules/rxjs/dist/types/internal/observable/generate.d.ts","./node_modules/rxjs/dist/types/internal/observable/iif.d.ts","./node_modules/rxjs/dist/types/internal/observable/interval.d.ts","./node_modules/rxjs/dist/types/internal/observable/merge.d.ts","./node_modules/rxjs/dist/types/internal/observable/never.d.ts","./node_modules/rxjs/dist/types/internal/observable/of.d.ts","./node_modules/rxjs/dist/types/internal/observable/onerrorresumenext.d.ts","./node_modules/rxjs/dist/types/internal/observable/pairs.d.ts","./node_modules/rxjs/dist/types/internal/observable/partition.d.ts","./node_modules/rxjs/dist/types/internal/observable/race.d.ts","./node_modules/rxjs/dist/types/internal/observable/range.d.ts","./node_modules/rxjs/dist/types/internal/observable/throwerror.d.ts","./node_modules/rxjs/dist/types/internal/observable/timer.d.ts","./node_modules/rxjs/dist/types/internal/observable/using.d.ts","./node_modules/rxjs/dist/types/internal/observable/zip.d.ts","./node_modules/rxjs/dist/types/internal/scheduled/scheduled.d.ts","./node_modules/rxjs/dist/types/internal/config.d.ts","./node_modules/rxjs/dist/types/index.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/rpc-exception-filter.interface.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/ws-exception-filter.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/validation-error.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/execution-context.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/can-activate.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/custom-route-param-factory.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/nest-interceptor.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/paramtype.interface.d.ts","./node_modules/@nestjs/common/interfaces/type.interface.d.ts","./node_modules/@nestjs/common/interfaces/features/pipe-transform.interface.d.ts","./node_modules/@nestjs/common/enums/request-method.enum.d.ts","./node_modules/@nestjs/common/enums/http-status.enum.d.ts","./node_modules/@nestjs/common/enums/shutdown-signal.enum.d.ts","./node_modules/@nestjs/common/enums/version-type.enum.d.ts","./node_modules/@nestjs/common/enums/index.d.ts","./node_modules/@nestjs/common/interfaces/version-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/middleware-configuration.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/middleware-consumer.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/middleware-config-proxy.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/nest-middleware.interface.d.ts","./node_modules/@nestjs/common/interfaces/middleware/index.d.ts","./node_modules/@nestjs/common/interfaces/global-prefix-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/before-application-shutdown.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-application-bootstrap.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-application-shutdown.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-destroy.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/on-init.interface.d.ts","./node_modules/@nestjs/common/interfaces/hooks/index.d.ts","./node_modules/@nestjs/common/interfaces/http/http-exception-body.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/http-redirect-response.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/cors-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/https-options.interface.d.ts","./node_modules/@nestjs/common/services/logger.service.d.ts","./node_modules/@nestjs/common/interfaces/nest-application-context-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-application-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/http-server.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/message-event.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/raw-body-request.interface.d.ts","./node_modules/@nestjs/common/interfaces/http/index.d.ts","./node_modules/@nestjs/common/interfaces/injectable.interface.d.ts","./node_modules/@nestjs/common/interfaces/microservices/nest-hybrid-application-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/forward-reference.interface.d.ts","./node_modules/@nestjs/common/interfaces/scope-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/injection-token.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/optional-factory-dependency.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/provider.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/module-metadata.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/dynamic-module.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/introspection-result.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/nest-module.interface.d.ts","./node_modules/@nestjs/common/interfaces/modules/index.d.ts","./node_modules/@nestjs/common/interfaces/shutdown-hooks-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-application-context.interface.d.ts","./node_modules/@nestjs/common/interfaces/websockets/web-socket-adapter.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-application.interface.d.ts","./node_modules/@nestjs/common/interfaces/nest-microservice.interface.d.ts","./node_modules/@nestjs/common/interfaces/index.d.ts","./node_modules/@nestjs/common/decorators/core/catch.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/controller.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/dependencies.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/exception-filters.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/inject.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/injectable.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/optional.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/set-metadata.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/use-guards.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/use-interceptors.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/use-pipes.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/apply-decorators.d.ts","./node_modules/@nestjs/common/decorators/core/version.decorator.d.ts","./node_modules/@nestjs/common/decorators/core/index.d.ts","./node_modules/@nestjs/common/decorators/modules/global.decorator.d.ts","./node_modules/@nestjs/common/decorators/modules/module.decorator.d.ts","./node_modules/@nestjs/common/decorators/modules/index.d.ts","./node_modules/@nestjs/common/decorators/http/request-mapping.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/route-params.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/http-code.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/create-route-param-metadata.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/render.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/header.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/redirect.decorator.d.ts","./node_modules/@nestjs/common/constants.d.ts","./node_modules/@nestjs/common/decorators/http/sse.decorator.d.ts","./node_modules/@nestjs/common/decorators/http/index.d.ts","./node_modules/@nestjs/common/decorators/index.d.ts","./node_modules/@nestjs/common/exceptions/intrinsic.exception.d.ts","./node_modules/@nestjs/common/exceptions/http.exception.d.ts","./node_modules/@nestjs/common/exceptions/bad-gateway.exception.d.ts","./node_modules/@nestjs/common/exceptions/bad-request.exception.d.ts","./node_modules/@nestjs/common/exceptions/conflict.exception.d.ts","./node_modules/@nestjs/common/exceptions/forbidden.exception.d.ts","./node_modules/@nestjs/common/exceptions/gateway-timeout.exception.d.ts","./node_modules/@nestjs/common/exceptions/gone.exception.d.ts","./node_modules/@nestjs/common/exceptions/http-version-not-supported.exception.d.ts","./node_modules/@nestjs/common/exceptions/im-a-teapot.exception.d.ts","./node_modules/@nestjs/common/exceptions/internal-server-error.exception.d.ts","./node_modules/@nestjs/common/exceptions/method-not-allowed.exception.d.ts","./node_modules/@nestjs/common/exceptions/misdirected.exception.d.ts","./node_modules/@nestjs/common/exceptions/not-acceptable.exception.d.ts","./node_modules/@nestjs/common/exceptions/not-found.exception.d.ts","./node_modules/@nestjs/common/exceptions/not-implemented.exception.d.ts","./node_modules/@nestjs/common/exceptions/payload-too-large.exception.d.ts","./node_modules/@nestjs/common/exceptions/precondition-failed.exception.d.ts","./node_modules/@nestjs/common/exceptions/request-timeout.exception.d.ts","./node_modules/@nestjs/common/exceptions/service-unavailable.exception.d.ts","./node_modules/@nestjs/common/exceptions/unauthorized.exception.d.ts","./node_modules/@nestjs/common/exceptions/unprocessable-entity.exception.d.ts","./node_modules/@nestjs/common/exceptions/unsupported-media-type.exception.d.ts","./node_modules/@nestjs/common/exceptions/index.d.ts","./node_modules/@nestjs/common/services/console-logger.service.d.ts","./node_modules/@nestjs/common/services/utils/filter-log-levels.util.d.ts","./node_modules/@nestjs/common/services/index.d.ts","./node_modules/@nestjs/common/file-stream/interfaces/streamable-options.interface.d.ts","./node_modules/@nestjs/common/file-stream/interfaces/streamable-handler-response.interface.d.ts","./node_modules/@nestjs/common/file-stream/interfaces/index.d.ts","./node_modules/@nestjs/common/file-stream/streamable-file.d.ts","./node_modules/@nestjs/common/file-stream/index.d.ts","./node_modules/@nestjs/common/module-utils/constants.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/configurable-module-async-options.interface.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/configurable-module-cls.interface.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/configurable-module-host.interface.d.ts","./node_modules/@nestjs/common/module-utils/interfaces/index.d.ts","./node_modules/@nestjs/common/module-utils/configurable-module.builder.d.ts","./node_modules/@nestjs/common/module-utils/index.d.ts","./node_modules/@nestjs/common/pipes/default-value.pipe.d.ts","./node_modules/@nestjs/common/pipes/file/interfaces/file.interface.d.ts","./node_modules/@nestjs/common/pipes/file/interfaces/index.d.ts","./node_modules/@nestjs/common/pipes/file/file-validator-context.interface.d.ts","./node_modules/@nestjs/common/pipes/file/file-validator.interface.d.ts","./node_modules/@nestjs/common/pipes/file/file-type.validator.d.ts","./node_modules/@nestjs/common/pipes/file/max-file-size.validator.d.ts","./node_modules/@nestjs/common/utils/http-error-by-code.util.d.ts","./node_modules/@nestjs/common/pipes/file/parse-file-options.interface.d.ts","./node_modules/@nestjs/common/pipes/file/parse-file.pipe.d.ts","./node_modules/@nestjs/common/pipes/file/parse-file-pipe.builder.d.ts","./node_modules/@nestjs/common/pipes/file/index.d.ts","./node_modules/@nestjs/common/interfaces/external/class-transform-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/transformer-package.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/validator-options.interface.d.ts","./node_modules/@nestjs/common/interfaces/external/validator-package.interface.d.ts","./node_modules/@nestjs/common/pipes/validation.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-array.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-bool.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-date.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-enum.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-float.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-int.pipe.d.ts","./node_modules/@nestjs/common/pipes/parse-uuid.pipe.d.ts","./node_modules/@nestjs/common/pipes/index.d.ts","./node_modules/@nestjs/common/serializer/class-serializer.interfaces.d.ts","./node_modules/@nestjs/common/serializer/class-serializer.interceptor.d.ts","./node_modules/@nestjs/common/serializer/decorators/serialize-options.decorator.d.ts","./node_modules/@nestjs/common/serializer/decorators/index.d.ts","./node_modules/@nestjs/common/serializer/index.d.ts","./node_modules/@nestjs/common/utils/forward-ref.util.d.ts","./node_modules/@nestjs/common/utils/index.d.ts","./node_modules/@nestjs/common/index.d.ts","./node_modules/@nestjs/config/dist/conditional.module.d.ts","./node_modules/@nestjs/config/dist/interfaces/config-change-event.interface.d.ts","./node_modules/@nestjs/config/dist/types/config-object.type.d.ts","./node_modules/@nestjs/config/dist/types/config.type.d.ts","./node_modules/@nestjs/config/dist/types/no-infer.type.d.ts","./node_modules/@nestjs/config/dist/types/path-value.type.d.ts","./node_modules/@nestjs/config/dist/types/index.d.ts","./node_modules/@nestjs/config/dist/interfaces/config-factory.interface.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/socks5-proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/iter.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/zlib/iter.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/dotenv-expand/lib/main.d.ts","./node_modules/@nestjs/config/dist/interfaces/config-module-options.interface.d.ts","./node_modules/@nestjs/config/dist/interfaces/index.d.ts","./node_modules/@nestjs/config/dist/config.module.d.ts","./node_modules/@nestjs/config/dist/config.service.d.ts","./node_modules/@nestjs/config/dist/utils/register-as.util.d.ts","./node_modules/@nestjs/config/dist/utils/get-config-token.util.d.ts","./node_modules/@nestjs/config/dist/utils/index.d.ts","./node_modules/@nestjs/config/dist/index.d.ts","./node_modules/@nestjs/config/index.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-basic.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-bearer.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/open-api-spec.interface.d.ts","./node_modules/@nestjs/swagger/dist/types/swagger-enum.type.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-body.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-consumes.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-cookie.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-default-getter.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-exclude-endpoint.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-include-endpoint.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-exclude-controller.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-extra-models.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-header.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-hide-property.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-link.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-oauth2.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-operation.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/enum-schema-attributes.interface.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-param.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-produces.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/schema-object-metadata.interface.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-property.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-query.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-webhook.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-response.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-security.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-use-tags.decorator.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/callback-object.interface.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-callbacks.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-extension.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/api-schema.decorator.d.ts","./node_modules/@nestjs/swagger/dist/decorators/index.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/swagger-ui-options.interface.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/swagger-custom-options.interface.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/swagger-document-options.interface.d.ts","./node_modules/@nestjs/swagger/dist/interfaces/index.d.ts","./node_modules/@nestjs/swagger/dist/document-builder.d.ts","./node_modules/@nestjs/swagger/dist/swagger-module.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/deep-partial-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/intersection-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/omit-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/partial-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/pick-type.helper.d.ts","./node_modules/@nestjs/swagger/dist/type-helpers/index.d.ts","./node_modules/@nestjs/swagger/dist/utils/get-schema-path.util.d.ts","./node_modules/@nestjs/swagger/dist/utils/generate-schema.util.d.ts","./node_modules/@nestjs/swagger/dist/utils/index.d.ts","./node_modules/@nestjs/swagger/dist/constants.d.ts","./node_modules/@nestjs/swagger/dist/index.d.ts","./src/common/tenant-id.decorator.ts","./node_modules/@prisma/client-runtime-utils/dist/index.d.ts","./node_modules/@prisma/client/runtime/client.d.ts","./node_modules/.prisma/client/index.d.ts","./node_modules/.prisma/client/default.d.ts","./node_modules/@prisma/client/default.d.ts","./node_modules/@prisma/debug/dist/index.d.ts","./node_modules/@prisma/driver-adapter-utils/dist/index.d.ts","./node_modules/pg-types/index.d.ts","./node_modules/pg-protocol/dist/messages.d.ts","./node_modules/pg-protocol/dist/serializer.d.ts","./node_modules/pg-protocol/dist/parser.d.ts","./node_modules/pg-protocol/dist/index.d.ts","./node_modules/@types/pg/lib/type-overrides.d.ts","./node_modules/@types/pg/index.d.ts","./node_modules/@prisma/adapter-pg/dist/index.d.ts","./src/prisma/prisma.service.ts","./src/audit/audit.service.ts","./src/audit/audit.controller.ts","./src/audit/audit.module.ts","./src/users/users.service.ts","./src/users/users.controller.ts","./src/users/users.module.ts","./src/auth/auth.service.ts","./src/auth/auth.controller.ts","./src/auth/auth.module.ts","./src/billing/billing.service.ts","./src/billing/billing.controller.ts","./src/billing/billing.module.ts","./src/channels/channels.service.ts","./src/channels/channels.controller.ts","./src/channels/channels.module.ts","./src/dictionaries/dictionaries.service.ts","./src/dictionaries/dictionaries.controller.ts","./src/dictionaries/dictionaries.module.ts","./node_modules/minio/dist/main/internal/copy-conditions.d.ts","./node_modules/minio/dist/main/internal/type.d.ts","./node_modules/minio/dist/main/helpers.d.ts","./node_modules/minio/dist/main/credentials.d.ts","./node_modules/minio/dist/main/credentialprovider.d.ts","./node_modules/eventemitter3/index.d.ts","./node_modules/minio/dist/main/notification.d.ts","./node_modules/minio/dist/main/internal/extensions.d.ts","./node_modules/minio/dist/main/internal/post-policy.d.ts","./node_modules/minio/dist/main/internal/s3-endpoints.d.ts","./node_modules/minio/dist/main/internal/xml-parser.d.ts","./node_modules/minio/dist/main/internal/client.d.ts","./node_modules/minio/dist/main/errors.d.ts","./node_modules/minio/dist/main/iamawsprovider.d.ts","./node_modules/minio/dist/main/minio.d.ts","./src/files/object-storage.service.ts","./src/files/files.service.ts","./src/files/files.controller.ts","./src/files/files.module.ts","./src/health.controller.ts","./src/prisma/prisma.module.ts","./src/sms-config/sms-config.service.ts","./src/sms-config/admin-sms-config.controller.ts","./src/sms-config/client-sms-config.controller.ts","./src/sms-config/sms-config.module.ts","./src/tenants/tenants.service.ts","./src/tenants/tenants.controller.ts","./src/tenants/tenants.module.ts","./src/app.module.ts","./node_modules/@nestjs/core/adapters/http-adapter.d.ts","./node_modules/@nestjs/core/adapters/index.d.ts","./node_modules/@nestjs/core/inspector/interfaces/edge.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/entrypoint.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/extras.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/node.interface.d.ts","./node_modules/@nestjs/core/injector/settlement-signal.d.ts","./node_modules/@nestjs/core/injector/injector.d.ts","./node_modules/@nestjs/core/inspector/interfaces/serialized-graph-metadata.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/serialized-graph-json.interface.d.ts","./node_modules/@nestjs/core/inspector/serialized-graph.d.ts","./node_modules/@nestjs/core/injector/opaque-key-factory/interfaces/module-opaque-key-factory.interface.d.ts","./node_modules/@nestjs/core/injector/compiler.d.ts","./node_modules/@nestjs/core/injector/modules-container.d.ts","./node_modules/@nestjs/core/injector/container.d.ts","./node_modules/@nestjs/core/injector/instance-links-host.d.ts","./node_modules/@nestjs/core/injector/abstract-instance-resolver.d.ts","./node_modules/@nestjs/core/injector/module-ref.d.ts","./node_modules/@nestjs/core/injector/module.d.ts","./node_modules/@nestjs/core/injector/instance-wrapper.d.ts","./node_modules/@nestjs/core/router/interfaces/exclude-route-metadata.interface.d.ts","./node_modules/@nestjs/core/application-config.d.ts","./node_modules/@nestjs/core/constants.d.ts","./node_modules/@nestjs/core/discovery/discovery-module.d.ts","./node_modules/@nestjs/core/discovery/discovery-service.d.ts","./node_modules/@nestjs/core/discovery/index.d.ts","./node_modules/@nestjs/core/helpers/http-adapter-host.d.ts","./node_modules/@nestjs/core/exceptions/base-exception-filter.d.ts","./node_modules/@nestjs/core/exceptions/index.d.ts","./node_modules/@nestjs/core/helpers/context-id-factory.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/exception-filter-metadata.interface.d.ts","./node_modules/@nestjs/core/exceptions/exceptions-handler.d.ts","./node_modules/@nestjs/core/router/router-proxy.d.ts","./node_modules/@nestjs/core/helpers/context-creator.d.ts","./node_modules/@nestjs/core/exceptions/base-exception-filter-context.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/rpc-exception-filter-metadata.interface.d.ts","./node_modules/@nestjs/common/interfaces/exceptions/index.d.ts","./node_modules/@nestjs/core/exceptions/external-exception-filter.d.ts","./node_modules/@nestjs/core/exceptions/external-exceptions-handler.d.ts","./node_modules/@nestjs/core/exceptions/external-exception-filter-context.d.ts","./node_modules/@nestjs/core/guards/constants.d.ts","./node_modules/@nestjs/core/helpers/execution-context-host.d.ts","./node_modules/@nestjs/core/guards/guards-consumer.d.ts","./node_modules/@nestjs/core/guards/guards-context-creator.d.ts","./node_modules/@nestjs/core/guards/index.d.ts","./node_modules/@nestjs/core/interceptors/interceptors-consumer.d.ts","./node_modules/@nestjs/core/interceptors/interceptors-context-creator.d.ts","./node_modules/@nestjs/core/interceptors/index.d.ts","./node_modules/@nestjs/common/enums/route-paramtypes.enum.d.ts","./node_modules/@nestjs/core/pipes/params-token-factory.d.ts","./node_modules/@nestjs/core/pipes/pipes-consumer.d.ts","./node_modules/@nestjs/core/pipes/pipes-context-creator.d.ts","./node_modules/@nestjs/core/pipes/index.d.ts","./node_modules/@nestjs/core/helpers/context-utils.d.ts","./node_modules/@nestjs/core/injector/inquirer/inquirer-constants.d.ts","./node_modules/@nestjs/core/injector/inquirer/index.d.ts","./node_modules/@nestjs/core/interfaces/module-definition.interface.d.ts","./node_modules/@nestjs/core/interfaces/module-override.interface.d.ts","./node_modules/@nestjs/core/inspector/interfaces/enhancer-metadata-cache-entry.interface.d.ts","./node_modules/@nestjs/core/inspector/graph-inspector.d.ts","./node_modules/@nestjs/core/metadata-scanner.d.ts","./node_modules/@nestjs/core/scanner.d.ts","./node_modules/@nestjs/core/injector/instance-loader.d.ts","./node_modules/@nestjs/core/injector/lazy-module-loader/lazy-module-loader-options.interface.d.ts","./node_modules/@nestjs/core/injector/lazy-module-loader/lazy-module-loader.d.ts","./node_modules/@nestjs/core/injector/index.d.ts","./node_modules/@nestjs/core/helpers/interfaces/external-handler-metadata.interface.d.ts","./node_modules/@nestjs/core/helpers/interfaces/params-metadata.interface.d.ts","./node_modules/@nestjs/core/helpers/external-context-creator.d.ts","./node_modules/@nestjs/core/helpers/index.d.ts","./node_modules/@nestjs/core/inspector/initialize-on-preview.allowlist.d.ts","./node_modules/@nestjs/core/inspector/partial-graph.host.d.ts","./node_modules/@nestjs/core/inspector/index.d.ts","./node_modules/@nestjs/core/middleware/route-info-path-extractor.d.ts","./node_modules/@nestjs/core/middleware/routes-mapper.d.ts","./node_modules/@nestjs/core/middleware/builder.d.ts","./node_modules/@nestjs/core/middleware/index.d.ts","./node_modules/@nestjs/core/nest-application-context.d.ts","./node_modules/@nestjs/core/nest-application.d.ts","./node_modules/@nestjs/common/interfaces/microservices/nest-microservice-options.interface.d.ts","./node_modules/@nestjs/core/nest-factory.d.ts","./node_modules/@nestjs/core/repl/repl.d.ts","./node_modules/@nestjs/core/repl/index.d.ts","./node_modules/@nestjs/core/router/interfaces/routes.interface.d.ts","./node_modules/@nestjs/core/router/interfaces/index.d.ts","./node_modules/@nestjs/core/router/request/request-constants.d.ts","./node_modules/@nestjs/core/router/request/index.d.ts","./node_modules/@nestjs/core/router/router-module.d.ts","./node_modules/@nestjs/core/router/index.d.ts","./node_modules/@nestjs/core/services/reflector.service.d.ts","./node_modules/@nestjs/core/services/index.d.ts","./node_modules/@nestjs/core/index.d.ts","./src/main.ts"],"fileIdsList":[[429,493,501,505,508,510,511,512,524,614],[429,493,501,505,508,510,511,512,524,613],[429,493,501,505,508,510,511,512,524],[318,429,493,501,505,508,510,511,512,524],[418,429,493,501,505,508,510,511,512,524],[67,319,320,321,322,323,324,325,326,327,328,329,330,331,429,493,501,505,508,510,511,512,524],[270,304,429,493,501,505,508,510,511,512,524],[277,429,493,501,505,508,510,511,512,524],[267,318,418,429,493,501,505,508,510,511,512,524],[336,337,338,339,340,341,342,344,429,493,501,505,508,510,511,512,524],[272,429,493,501,505,508,510,511,512,524],[318,418,429,493,501,505,508,510,511,512,524],[272,343,429,493,501,505,508,510,511,512,524],[332,335,345,429,493,501,505,508,510,511,512,524],[333,334,429,493,501,505,508,510,511,512,524],[308,429,493,501,505,508,510,511,512,524],[272,273,274,275,429,493,501,505,508,510,511,512,524],[348,429,493,501,505,508,510,511,512,524],[290,347,429,493,501,505,508,510,511,512,524],[347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,429,493,501,505,508,510,511,512,524],[377,429,493,501,505,508,510,511,512,524],[374,375,429,493,501,505,508,510,511,512,524],[373,376,429,493,501,505,508,510,511,512,524,529],[66,276,318,346,370,373,378,385,410,415,417,429,493,501,505,508,510,511,512,524],[72,270,429,493,501,505,508,510,511,512,524],[71,429,493,501,505,508,510,511,512,524],[72,262,263,429,493,501,505,508,510,511,512,524,705,710],[262,270,429,493,501,505,508,510,511,512,524],[71,261,429,493,501,505,508,510,511,512,524],[270,398,429,493,501,505,508,510,511,512,524],[264,400,429,493,501,505,508,510,511,512,524],[261,265,429,493,501,505,508,510,511,512,524],[265,429,493,501,505,508,510,511,512,524],[71,318,429,493,501,505,508,510,511,512,524],[269,270,429,493,501,505,508,510,511,512,524],[282,429,493,501,505,508,510,511,512,524],[284,285,286,287,288,429,493,501,505,508,510,511,512,524],[276,429,493,501,505,508,510,511,512,524],[276,277,296,429,493,501,505,508,510,511,512,524],[290,291,297,298,299,429,493,501,505,508,510,511,512,524],[68,69,70,71,72,262,263,264,265,266,267,268,269,270,271,277,282,283,289,296,300,301,302,304,312,313,314,315,316,317,429,493,501,505,508,510,511,512,524],[295,429,493,501,505,508,510,511,512,524],[278,279,280,281,429,493,501,505,508,510,511,512,524],[270,278,279,429,493,501,505,508,510,511,512,524],[270,276,277,429,493,501,505,508,510,511,512,524],[270,280,429,493,501,505,508,510,511,512,524],[270,308,429,493,501,505,508,510,511,512,524],[303,305,306,307,308,309,310,311,429,493,501,505,508,510,511,512,524],[68,270,429,493,501,505,508,510,511,512,524],[304,429,493,501,505,508,510,511,512,524],[68,270,303,307,309,429,493,501,505,508,510,511,512,524],[279,429,493,501,505,508,510,511,512,524],[305,429,493,501,505,508,510,511,512,524],[270,304,305,306,429,493,501,505,508,510,511,512,524],[294,429,493,501,505,508,510,511,512,524],[270,274,294,295,312,313,429,493,501,505,508,510,511,512,524],[292,293,295,429,493,501,505,508,510,511,512,524],[266,268,277,283,297,314,315,318,429,493,501,505,508,510,511,512,524],[72,261,266,268,271,314,315,429,493,501,505,508,510,511,512,524],[275,429,493,501,505,508,510,511,512,524],[261,429,493,501,505,508,510,511,512,524],[294,318,379,383,429,493,501,505,508,510,511,512,524],[383,384,429,493,501,505,508,510,511,512,524],[318,379,429,493,501,505,508,510,511,512,524],[318,379,380,429,493,501,505,508,510,511,512,524],[380,381,429,493,501,505,508,510,511,512,524],[380,381,382,429,493,501,505,508,510,511,512,524],[271,429,493,501,505,508,510,511,512,524],[388,389,390,429,493,501,505,508,510,511,512,524],[388,429,493,501,505,508,510,511,512,524],[390,391,392,394,395,396,429,493,501,505,508,510,511,512,524],[387,429,493,501,505,508,510,511,512,524],[390,393,429,493,501,505,508,510,511,512,524],[390,391,392,394,395,429,493,501,505,508,510,511,512,524],[271,388,390,394,429,493,501,505,508,510,511,512,524],[386,397,402,403,404,405,406,407,408,409,429,493,501,505,508,510,511,512,524],[271,318,402,429,493,501,505,508,510,511,512,524],[271,393,429,493,501,505,508,510,511,512,524],[271,393,418,429,493,501,505,508,510,511,512,524],[264,270,271,393,398,399,400,401,429,493,501,505,508,510,511,512,524],[261,318,398,399,411,429,493,501,505,508,510,511,512,524],[318,398,429,493,501,505,508,510,511,512,524],[413,429,493,501,505,508,510,511,512,524],[346,411,429,493,501,505,508,510,511,512,524],[411,412,414,429,493,501,505,508,510,511,512,524],[294,429,493,501,505,508,510,511,512,524,543],[294,371,372,429,493,501,505,508,510,511,512,524],[303,429,493,501,505,508,510,511,512,524],[276,318,429,493,501,505,508,510,511,512,524],[416,429,493,501,505,508,510,511,512,524],[418,429,493,501,505,508,510,511,512,524,554],[261,420,425,429,493,501,505,508,510,511,512,524],[419,425,429,493,501,505,508,510,511,512,524,554,555,556,559],[425,429,493,501,505,508,510,511,512,524],[426,429,493,501,505,508,510,511,512,524,552],[420,426,429,493,501,505,508,510,511,512,524,553],[421,422,423,424,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,557,558],[425,429,493,501,505,508,510,511,512,524,554,560],[429,493,501,505,508,510,511,512,524,560],[296,318,418,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,675],[318,418,429,493,501,505,508,510,511,512,524,694,695],[343,429,493,501,505,508,510,511,512,524],[418,429,493,501,505,508,510,511,512,524,688,693,694],[429,493,501,505,508,510,511,512,524,698,699],[72,318,429,493,501,505,508,510,511,512,524,689,694,708],[418,429,493,501,505,508,510,511,512,524,676,701],[71,418,429,493,501,505,508,510,511,512,524,702,705],[318,429,493,501,505,508,510,511,512,524,689,694,696,707,709,713],[71,429,493,501,505,508,510,511,512,524,711,712],[429,493,501,505,508,510,511,512,524,702],[261,318,418,429,493,501,505,508,510,511,512,524,716],[318,418,429,493,501,505,508,510,511,512,524,689,694,696,708],[429,493,501,505,508,510,511,512,524,715,717,718],[318,429,493,501,505,508,510,511,512,524,694],[429,493,501,505,508,510,511,512,524,694],[318,418,429,493,501,505,508,510,511,512,524,716],[71,318,418,429,493,501,505,508,510,511,512,524],[318,418,429,493,501,505,508,510,511,512,524,688,689,694,714,716,719,722,727,728,741,742],[261,429,493,501,505,508,510,511,512,524,675],[429,493,501,505,508,510,511,512,524,701,704,743],[429,493,501,505,508,510,511,512,524,728,740],[66,429,493,501,505,508,510,511,512,524,676,696,697,700,703,735,740,744,747,751,752,753,755,757,763,765],[318,418,429,493,501,505,508,510,511,512,524,682,690,693,694],[318,429,493,501,505,508,510,511,512,524,686],[295,318,343,418,429,493,501,505,508,510,511,512,524,685,686,687,688,693,694,696,766],[429,493,501,505,508,510,511,512,524,688,689,692,694,730,739],[318,418,429,493,501,505,508,510,511,512,524,681,693,694],[429,493,501,505,508,510,511,512,524,729],[418,429,493,501,505,508,510,511,512,524,689,694],[418,429,493,501,505,508,510,511,512,524,682,689,693,734],[318,343,418,429,493,501,505,508,510,511,512,524,681,693],[418,429,493,501,505,508,510,511,512,524,687,688,692,732,736,737,738],[418,429,493,501,505,508,510,511,512,524,682,689,690,691,693,694],[318,343,429,493,501,505,508,510,511,512,524,689,692,694],[261,429,493,501,505,508,510,511,512,524,693],[270,303,309,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,678,679,680,689,693,694,733],[429,493,501,505,508,510,511,512,524,685,734,745,746],[343,418,429,493,501,505,508,510,511,512,524,694],[343,418,429,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,677,678,679,680,683,685],[429,493,501,505,508,510,511,512,524,682],[429,493,501,505,508,510,511,512,524,684,685],[418,429,493,501,505,508,510,511,512,524,677,678,679,680,683,684],[429,493,501,505,508,510,511,512,524,720,721],[318,429,493,501,505,508,510,511,512,524,689,694,696,708],[429,493,501,505,508,510,511,512,524,731],[301,429,493,501,505,508,510,511,512,524],[282,318,429,493,501,505,508,510,511,512,524,748,749],[429,493,501,505,508,510,511,512,524,750],[318,429,493,501,505,508,510,511,512,524,696],[318,429,493,501,505,508,510,511,512,524,689,696],[295,318,418,429,493,501,505,508,510,511,512,524,682,689,690,691,693,694],[294,318,418,429,493,501,505,508,510,511,512,524,676,689,696,734,752],[295,296,418,429,493,501,505,508,510,511,512,524,675,754],[429,493,501,505,508,510,511,512,524,724,725,726],[418,429,493,501,505,508,510,511,512,524,723],[429,493,501,505,508,510,511,512,524,756],[418,429,493,501,505,508,510,511,512,524,526],[429,493,501,505,508,510,511,512,524,759,761,762],[429,493,501,505,508,510,511,512,524,758],[429,493,501,505,508,510,511,512,524,760],[418,429,493,501,505,508,510,511,512,524,688,693,759],[429,493,501,505,508,510,511,512,524,706],[318,343,418,429,493,501,505,508,510,511,512,524,689,693,694,696,731,732,734,735],[429,493,501,505,508,510,511,512,524,764],[418,429,493,501,505,508,510,511,512,524,564,565],[429,493,501,505,508,510,511,512,524,589],[429,493,501,505,508,510,511,512,524,564,565],[429,493,501,505,508,510,511,512,524,564],[418,429,493,501,505,508,510,511,512,524,564,565,579],[418,429,493,501,505,508,510,511,512,524,579,582],[418,429,493,501,505,508,510,511,512,524,564],[429,493,501,505,508,510,511,512,524,582],[429,493,501,505,508,510,511,512,524,562,563,566,567,568,569,570,571,572,573,574,575,576,577,578,580,581,583,584,585,586,587,588,590,591,592],[429,493,501,505,508,510,511,512,524,564,586,597],[66,429,493,501,505,508,510,511,512,524,593,597,598,599,605,608,609],[429,493,501,505,508,510,511,512,524,564,595,596],[418,429,493,501,505,508,510,511,512,524,564,579],[429,493,501,505,508,510,511,512,524,564,594],[297,418,429,493,501,505,508,510,511,512,524,597],[429,493,501,505,508,510,511,512,524,600,601,602,603,604],[429,493,501,505,508,510,511,512,524,606,607],[429,493,501,505,508,510,511,512,524,618,625],[429,493,501,505,508,510,511,512,524,615],[429,493,501,505,508,510,511,512,524,612],[429,493,501,505,508,510,511,512,524,617],[429,490,491,493,501,505,508,510,511,512,524],[429,492,493,501,505,508,510,511,512,524],[493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,533],[429,493,494,499,501,504,505,508,510,511,512,514,524,529,542],[429,493,494,495,501,504,505,508,510,511,512,524],[429,493,496,501,505,508,510,511,512,524,543],[429,493,497,498,501,505,508,510,511,512,515,524],[429,493,498,501,505,508,510,511,512,524,529,539],[429,493,499,501,504,505,508,510,511,512,514,524],[429,492,493,500,501,505,508,510,511,512,524],[429,493,501,502,505,508,510,511,512,524],[429,493,501,503,504,505,508,510,511,512,524],[429,492,493,501,504,505,508,510,511,512,524],[429,493,501,504,505,506,508,510,511,512,524,529,542],[429,493,501,504,505,506,508,510,511,512,524,529,531,533],[429,480,493,501,504,505,507,508,510,511,512,514,524,529,542],[429,493,501,504,505,507,508,510,511,512,514,524,529,539,542],[429,493,501,505,507,508,509,510,511,512,524,529,539,542],[427,428,429,430,431,432,433,434,435,436,437,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550],[429,493,501,504,505,508,510,511,512,524],[429,493,501,505,508,510,512,524],[429,493,501,505,508,510,511,512,513,524,542],[429,493,501,504,505,508,510,511,512,514,524,529],[429,493,501,505,508,510,511,512,515,524],[429,493,501,505,508,510,511,512,516,524],[429,493,501,504,505,508,510,511,512,519,524],[429,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549],[429,493,501,505,508,510,511,512,521,524],[429,493,501,505,508,510,511,512,522,524],[429,493,498,501,505,508,510,511,512,514,524,533],[429,493,501,504,505,508,510,511,512,524,525],[429,493,501,505,508,510,511,512,524,526,543,546],[429,493,501,504,505,508,510,511,512,524,529,532,533],[429,493,501,505,508,510,511,512,524,530,533],[429,493,501,505,508,510,511,512,524,531],[429,493,501,505,508,510,511,512,524,533,543],[429,493,501,505,508,510,511,512,524,534],[429,490,493,501,505,508,510,511,512,524,529,536,542],[429,493,501,505,508,510,511,512,524,529,535],[429,493,501,504,505,508,510,511,512,524,537,538],[429,493,501,505,508,510,511,512,524,537,538],[429,493,498,501,505,508,510,511,512,514,524,529,539],[429,493,501,505,508,510,511,512,524,540],[429,493,501,505,508,510,511,512,514,524,541],[429,493,501,505,507,508,510,511,512,522,524,542],[429,493,501,505,508,510,511,512,524,543,544],[429,493,498,501,505,508,510,511,512,524,544],[429,493,501,505,508,510,511,512,524,529,545],[429,493,501,505,508,510,511,512,513,524,546],[429,493,501,505,508,510,511,512,524,547],[429,493,496,501,505,508,510,511,512,524],[429,493,498,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,543],[429,480,493,501,505,508,510,511,512,524],[429,493,501,505,508,510,511,512,524,542],[429,493,501,505,508,510,511,512,524,548],[429,493,501,505,508,510,511,512,519,524],[429,493,501,505,508,510,511,512,524,538],[429,480,493,501,504,505,506,508,510,511,512,519,524,529,533,542,545,546,548],[429,493,501,505,508,510,511,512,524,529,549],[429,493,501,505,508,510,511,512,524,531,550],[429,493,501,504,505,508,510,511,512,524,529,539,551,619,620,623,624,625],[429,493,501,505,508,510,511,512,524,625],[429,493,501,505,508,510,511,512,524,551],[429,493,501,505,508,510,511,512,524,649],[429,493,501,505,508,510,511,512,524,647],[429,493,501,505,507,508,510,511,512,524,551,649,650],[429,493,501,505,507,508,509,510,511,512,524,529,551,646,647,648,650,652,653,654,655,656],[429,493,501,505,508,510,511,512,524,647,657],[429,493,501,505,507,508,510,511,512,524,529,551,646,648],[429,493,501,505,507,508,510,511,512,524,551,647,648],[429,493,501,505,508,510,511,512,524,646,647,648,652,654,655,657,658,659],[429,493,501,505,508,510,511,512,524,651,657],[429,493,501,505,508,510,511,512,524,620,621,622],[429,493,501,505,508,510,511,512,524,529,620],[73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,89,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,129,130,131,132,133,134,135,136,137,138,139,140,142,143,144,145,146,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,192,193,194,196,205,207,208,209,210,211,212,214,215,217,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,429,493,501,505,508,510,511,512,524],[118,429,493,501,505,508,510,511,512,524],[74,77,429,493,501,505,508,510,511,512,524],[76,429,493,501,505,508,510,511,512,524],[76,77,429,493,501,505,508,510,511,512,524],[73,74,75,77,429,493,501,505,508,510,511,512,524],[74,76,77,234,429,493,501,505,508,510,511,512,524],[77,429,493,501,505,508,510,511,512,524],[73,76,118,429,493,501,505,508,510,511,512,524],[76,77,234,429,493,501,505,508,510,511,512,524],[76,242,429,493,501,505,508,510,511,512,524],[74,76,77,429,493,501,505,508,510,511,512,524],[86,429,493,501,505,508,510,511,512,524],[109,429,493,501,505,508,510,511,512,524],[130,429,493,501,505,508,510,511,512,524],[76,77,118,429,493,501,505,508,510,511,512,524],[77,125,429,493,501,505,508,510,511,512,524],[76,77,118,136,429,493,501,505,508,510,511,512,524],[76,77,136,429,493,501,505,508,510,511,512,524],[77,177,429,493,501,505,508,510,511,512,524],[77,118,429,493,501,505,508,510,511,512,524],[73,77,195,429,493,501,505,508,510,511,512,524],[73,77,196,429,493,501,505,508,510,511,512,524],[218,429,493,501,505,508,510,511,512,524],[202,204,429,493,501,505,508,510,511,512,524],[213,429,493,501,505,508,510,511,512,524],[202,429,493,501,505,508,510,511,512,524],[73,77,195,202,203,429,493,501,505,508,510,511,512,524],[195,196,204,429,493,501,505,508,510,511,512,524],[216,429,493,501,505,508,510,511,512,524],[73,77,202,203,204,429,493,501,505,508,510,511,512,524],[75,76,77,429,493,501,505,508,510,511,512,524],[73,77,429,493,501,505,508,510,511,512,524],[74,76,196,197,198,199,429,493,501,505,508,510,511,512,524],[118,196,197,198,199,429,493,501,505,508,510,511,512,524],[196,198,429,493,501,505,508,510,511,512,524],[76,197,198,200,201,205,429,493,501,505,508,510,511,512,524],[73,76,429,493,501,505,508,510,511,512,524],[77,220,429,493,501,505,508,510,511,512,524],[78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,126,127,128,129,130,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,429,493,501,505,508,510,511,512,524],[206,429,493,501,505,508,510,511,512,524],[429,444,447,450,451,493,501,505,508,510,511,512,524,542],[429,447,493,501,505,508,510,511,512,524,529,542],[429,447,451,493,501,505,508,510,511,512,524,542],[429,493,501,505,508,510,511,512,524,529],[429,441,493,501,505,508,510,511,512,524],[429,445,493,501,505,508,510,511,512,524],[429,443,444,447,493,501,505,508,510,511,512,524,542],[429,493,501,505,508,510,511,512,514,524,539],[429,441,493,501,505,508,510,511,512,524,551],[429,443,447,493,501,505,508,510,511,512,514,524,542],[429,438,439,440,442,446,493,501,504,505,508,510,511,512,524,529,542],[429,447,456,464,493,501,505,508,510,511,512,524],[429,439,445,493,501,505,508,510,511,512,524],[429,447,474,475,493,501,505,508,510,511,512,524],[429,439,442,447,493,501,505,508,510,511,512,524,533,542,551],[429,447,493,501,505,508,510,511,512,524],[429,443,447,493,501,505,508,510,511,512,524,542],[429,438,493,501,505,508,510,511,512,524],[429,441,442,443,445,446,447,448,449,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,475,476,477,478,479,493,501,505,508,510,511,512,524],[429,447,467,470,493,501,505,508,510,511,512,524],[429,447,456,457,458,493,501,505,508,510,511,512,524],[429,445,447,457,459,493,501,505,508,510,511,512,524],[429,446,493,501,505,508,510,511,512,524],[429,439,441,447,493,501,505,508,510,511,512,524],[429,447,451,457,459,493,501,505,508,510,511,512,524],[429,451,493,501,505,508,510,511,512,524],[429,445,447,450,493,501,505,508,510,511,512,524,542],[429,439,443,447,456,493,501,505,508,510,511,512,524],[429,447,467,493,501,505,508,510,511,512,524],[429,459,493,501,505,508,510,511,512,524],[429,439,443,447,451,493,501,505,508,510,511,512,524],[429,441,447,474,493,501,505,508,510,511,512,524,533,548,551],[418,429,493,501,505,508,510,511,512,524,561,630,633,636,639,642,645,664,665,666,670,673],[418,429,493,501,505,508,510,511,512,524,610,611,628],[418,429,493,501,505,508,510,511,512,524,628,629],[418,429,493,501,505,508,510,511,512,524,616,627],[418,429,493,501,505,508,510,511,512,524,610,634],[418,429,493,501,505,508,510,511,512,524,633,634,635],[418,429,493,501,505,508,510,511,512,524,631],[418,429,493,501,505,508,510,511,512,524,610,611,637],[418,429,493,501,505,508,510,511,512,524,637,638],[418,429,493,501,505,508,510,511,512,524,610,640],[418,429,493,501,505,508,510,511,512,524,640,641],[418,429,493,501,505,508,510,511,512,524,610,611,643],[418,429,493,501,505,508,510,511,512,524,643,644],[418,429,493,501,505,508,510,511,512,524,610,611,662],[418,429,493,501,505,508,510,511,512,524,661,662,663],[418,429,493,501,505,508,510,511,512,524,616,627,661],[418,429,493,501,505,508,510,511,512,524,561,660],[418,429,493,501,505,508,510,511,512,524,610],[66,429,493,501,505,508,510,511,512,524,610,674,766],[418,429,493,501,505,508,510,511,512,524,627],[418,429,493,501,505,508,510,511,512,524,616,626],[418,429,493,501,505,508,510,511,512,524,610,667],[418,429,493,501,505,508,510,511,512,524,610,611,667],[418,429,493,501,505,508,510,511,512,524,667,668,669],[418,429,493,498,501,505,508,510,511,512,524,616,627],[418,429,493,501,505,508,510,511,512,524,610,671],[418,429,493,501,505,508,510,511,512,524,671,672],[418,429,493,501,505,508,510,511,512,524,610,611,631],[418,429,493,501,505,508,510,511,512,524,631,632],[418,429,493,498,501,505,508,510,511,512,524,627]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"2a2de5b9459b3fc44decd9ce6100b72f1b002ef523126c1d3d8b2a4a63d74d78","affectsGlobalScope":true,"impliedFormat":1},{"version":"f13f4b465c99041e912db5c44129a94588e1aafee35a50eab51044833f50b4ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"01a30f9e8582b369075c0808df71121e6855cb06fd8d3d39511d9ebb66405205","impliedFormat":1},{"version":"8d6d51a5118d000ed3bfe6e1dd1335bebfff3fef23cd2af2f84a24d30f90cc90","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d8dedbec739bc79642c1e96e9bfc0b83b25b104a0486aebf016fc7b85b39f48","impliedFormat":1},{"version":"e89535c3ec439608bcd0f68af555d0e5ddf121c54abe69343549718bd7506b9c","impliedFormat":1},{"version":"622a984b60c294ffb2f9152cf1d4d12e91d2b733d820eec949cf54d63a3c1025","impliedFormat":1},{"version":"81aae92abdeaccd9c1723cef39232c90c1aed9d9cf199e6e2a523b7d8e058a11","impliedFormat":1},{"version":"a63a6c6806a1e519688ef7bd8ca57be912fc0764485119dbd923021eb4e79665","impliedFormat":1},{"version":"75b57b109d774acca1e151df21cf5cb54c7a1df33a273f0457b9aee4ebd36fb9","impliedFormat":1},{"version":"073ca26c96184db9941b5ec0ddea6981c9b816156d9095747809e524fdd90e35","impliedFormat":1},{"version":"e41d17a2ec23306d953cda34e573ed62954ca6ea9b8c8b74e013d07a6886ce47","impliedFormat":1},{"version":"241bd4add06f06f0699dcd58f3b334718d85e3045d9e9d4fa556f11f4d1569c1","impliedFormat":1},{"version":"2ae3787e1498b20aad1b9c2ee9ea517ec30e89b70d242d8e3e52d1e091039695","impliedFormat":1},{"version":"c7c72c4cffb1bc83617eefed71ed68cc89df73cab9e19507ccdecb3e72b4967e","affectsGlobalScope":true,"impliedFormat":1},{"version":"b8bff8a60af0173430b18d9c3e5c443eaa3c515617210c0c7b3d2e1743c19ecb","impliedFormat":1},{"version":"38b38db08e7121828294dec10957a7a9ff263e33e2a904b346516d4a4acca482","impliedFormat":1},{"version":"a76ebdf2579e68e4cfe618269c47e5a12a4e045c2805ed7f7ab37af8daa6b091","impliedFormat":1},{"version":"8a2aaea564939c22be05d665cc955996721bad6d43148f8fa21ae8f64afecd37","impliedFormat":1},{"version":"e59d36b7b6e8ba2dd36d032a5f5c279d2460968c8b4e691ca384f118fb09b52a","impliedFormat":1},{"version":"e96885c0684c9042ec72a9a43ef977f6b4b4a2728f4b9e737edcbaa0c74e5bf6","impliedFormat":1},{"version":"95950a187596e206d32d5d9c7b932901088c65ed8f9040e614aa8e321e0225ef","impliedFormat":1},{"version":"89e061244da3fc21b7330f4bd32f47c1813dd4d7f1dc3d0883d88943f035b993","impliedFormat":1},{"version":"e46558c2e04d06207b080138678020448e7fc201f3d69c2601b0d1456105f29a","impliedFormat":1},{"version":"71549375db52b1163411dba383b5f4618bdf35dc57fa327a1c7d135cf9bf67d1","impliedFormat":1},{"version":"7e6b2d61d6215a4e82ea75bc31a80ebb8ad0c2b37a60c10c70dd671e8d9d6d5d","impliedFormat":1},{"version":"78bea05df2896083cca28ed75784dde46d4b194984e8fc559123b56873580a23","impliedFormat":1},{"version":"5dd04ced37b7ea09f29d277db11f160df7fd73ba8b9dba86cb25552e0653a637","impliedFormat":1},{"version":"f74b81712e06605677ae1f061600201c425430151f95b5ef4d04387ad7617e6a","impliedFormat":1},{"version":"9a72847fcf4ac937e352d40810f7b7aec7422d9178451148296cf1aa19467620","impliedFormat":1},{"version":"3ae18f60e0b96fa1e025059b7d25b3247ba4dcb5f4372f6d6e67ce2adac74eac","impliedFormat":1},{"version":"2b9260f44a2e071450ae82c110f5dc8f330c9e5c3e85567ed97248330f2bf639","impliedFormat":1},{"version":"4f196e13684186bda6f5115fc4677a87cf84a0c9c4fc17b8f51e0984f3697b6d","impliedFormat":1},{"version":"61419f2c5822b28c1ea483258437c1faab87d00c6f84481aa22afb3380d8e9a4","impliedFormat":1},{"version":"64479aee03812264e421c0bf5104a953ca7b02740ba80090aead1330d0effe91","impliedFormat":1},{"version":"0521108c9f8ddb17654a0a54dae6ba9667c99eddccfd6af5748113e022d1c37a","impliedFormat":1},{"version":"c5570e504be103e255d80c60b56c367bf45d502ca52ee35c55dec882f6563b5c","impliedFormat":1},{"version":"ee764e6e9a7f2b987cc1a2c0a9afd7a8f4d5ebc4fdb66ad557a7f14a8c2bd320","impliedFormat":1},{"version":"0520b5093712c10c6ef23b5fea2f833bf5481771977112500045e5ea7e8e2b69","impliedFormat":1},{"version":"5c3cf26654cf762ac4d7fd7b83f09acfe08eef88d2d6983b9a5a423cb4004ca3","impliedFormat":1},{"version":"e60fa19cf7911c1623b891155d7eb6b7e844e9afdf5738e3b46f3b687730a2bd","impliedFormat":1},{"version":"b1fd72ff2bb0ba91bb588f3e5329f8fc884eb859794f1c4657a2bfa122ae54d0","impliedFormat":1},{"version":"6cf42a4f3cfec648545925d43afaa8bb364ac10a839ffed88249da109361b275","impliedFormat":1},{"version":"d7058e75920120b142a9d57be25562a3cd9a936269fd52908505f530105f2ec4","impliedFormat":1},{"version":"6df52b70d7f7702202f672541a5f4a424d478ee5be51a9d37b8ccbe1dbf3c0f2","impliedFormat":1},{"version":"0ca7f997e9a4d8985e842b7c882e521b6f63233c4086e9fe79dd7a9dc4742b5e","impliedFormat":1},{"version":"91046b5c6b55d3b194c81fd4df52f687736fad3095e9d103ead92bb64dc160ee","impliedFormat":1},{"version":"db5704fdad56c74dfc5941283c1182ed471bd17598209d3ac4a49faa72e43cfc","impliedFormat":1},{"version":"758e8e89559b02b81bc0f8fd395b17ad5aff75490c862cbe369bb1a3d1577c40","impliedFormat":1},{"version":"2ee64342c077b1868f1834c063f575063051edd6e2964257d34aad032d6b657c","impliedFormat":1},{"version":"6f6b4b3d670b6a5f0e24ea001c1b3d36453c539195e875687950a178f1730fa7","impliedFormat":1},{"version":"a472a1d3f25ce13a1d44911cd3983956ac040ce2018e155435ea34afb25f864c","impliedFormat":1},{"version":"b48b83a86dd9cfe36f8776b3ff52fcd45b0e043c0538dc4a4b149ba45fe367b9","impliedFormat":1},{"version":"792de5c062444bd2ee0413fb766e57e03cce7cdaebbfc52fc0c7c8e95069c96b","impliedFormat":1},{"version":"a79e3e81094c7a04a885bad9b049c519aace53300fb8a0fe4f26727cb5a746ce","impliedFormat":1},{"version":"93181bac0d90db185bb730c95214f6118ae997fe836a98a49664147fbcaf1988","impliedFormat":1},{"version":"8a4e89564d8ea66ad87ee3762e07540f9f0656a62043c910d819b4746fc429c5","impliedFormat":1},{"version":"b9011d99942889a0f95e120d06b698c628b0b6fdc3e6b7ecb459b97ed7d5bcc6","impliedFormat":1},{"version":"4d639cbbcc2f8f9ce6d55d5d503830d6c2556251df332dc5255d75af53c8a0e7","impliedFormat":1},{"version":"cdb48277f600ab5f429ecf1c5ea046683bc6b9f73f3deab9a100adac4b34969c","impliedFormat":1},{"version":"75be84956a29040a1afbe864c0a7a369dfdb739380072484eff153905ef867ee","impliedFormat":1},{"version":"b06b4adc2ae03331a92abd1b19af8eb91ec2bf8541747ee355887a167d53145e","impliedFormat":1},{"version":"c54166a85bd60f86d1ebb90ce0117c0ecb850b8a33b366691629fdf26f1bbbd8","impliedFormat":1},{"version":"0d417c15c5c635384d5f1819cc253a540fe786cc3fda32f6a2ae266671506a21","impliedFormat":1},{"version":"80f23f1d60fbed356f726b3b26f9d348dddbb34027926d10d59fad961e70a730","impliedFormat":1},{"version":"cb59317243a11379a101eb2f27b9df1022674c3df1df0727360a0a3f963f523b","impliedFormat":1},{"version":"cc20bb2227dd5de0aab0c8d697d1572f8000550e62c7bf5c92f212f657dd88c5","impliedFormat":1},{"version":"06b8a7d46195b6b3980e523ef59746702fd210b71681a83a5cf73799623621f9","impliedFormat":1},{"version":"860e4405959f646c101b8005a191298b2381af8f33716dc5f42097e4620608f8","impliedFormat":1},{"version":"f7e32adf714b8f25d3c1783473abec3f2e82d5724538d8dcf6f51baaaff1ca7a","impliedFormat":1},{"version":"d0da80c845999a16c24d0783033fb5366ada98df17867c98ad433ede05cd87fd","impliedFormat":1},{"version":"bfbf80f9cd4558af2d7b2006065340aaaced15947d590045253ded50aabb9bc5","impliedFormat":1},{"version":"fd9a991b51870325e46ebb0e6e18722d313f60cd8e596e645ec5ac15b96dbf4e","impliedFormat":1},{"version":"c3bd2b94e4298f81743d92945b80e9b56c1cdfb2bef43c149b7106a2491b1fc9","impliedFormat":1},{"version":"a246cce57f558f9ebaffd55c1e5673da44ea603b4da3b2b47eb88915d30a9181","impliedFormat":1},{"version":"d993eacc103c5a065227153c9aae8acea3a4322fe1a169ee7c70b77015bf0bb2","impliedFormat":1},{"version":"fc2b03d0c042aa1627406e753a26a1eaad01b3c496510a78016822ef8d456bb6","impliedFormat":1},{"version":"063c7ebbe756f0155a8b453f410ca6b76ffa1bbc1048735bcaf9c7c81a1ce35f","impliedFormat":1},{"version":"314e402cd481370d08f63051ae8b8c8e6370db5ee3b8820eeeaaf8d722a6dac6","impliedFormat":1},{"version":"9669075ac38ce36b638b290ba468233980d9f38bdc62f0519213b2fd3e2552ec","impliedFormat":1},{"version":"4d123de012c24e2f373925100be73d50517ac490f9ed3578ac82d0168bfbd303","impliedFormat":1},{"version":"656c9af789629aa36b39092bee3757034009620439d9a39912f587538033ce28","impliedFormat":1},{"version":"3ac3f4bdb8c0905d4c3035d6f7fb20118c21e8a17bee46d3735195b0c2a9f39f","impliedFormat":1},{"version":"1f453e6798ed29c86f703e9b41662640d4f2e61337007f27ac1c616f20093f69","impliedFormat":1},{"version":"af43b7871ff21c62bf1a54ec5c488e31a8d3408d5b51ff2e9f8581b6c55f2fc7","impliedFormat":1},{"version":"70550511d25cbb0b6a64dcac7fffc3c1397fd4cbeb6b23ccc7f9b794ab8a6954","impliedFormat":1},{"version":"af0fbf08386603a62f2a78c42d998c90353b1f1d22e05a384545f7accf881e0a","impliedFormat":1},{"version":"cefc20054d20b85b534206dbcedd509bb74f87f3d8bc45c58c7be3a76caa45e1","impliedFormat":1},{"version":"ad6eee4877d0f7e5244d34bc5026fd6e9cf8e66c5c79416b73f9f6ebf132f924","impliedFormat":1},{"version":"4888fd2bcfee9a0ce89d0df860d233e0cee8ee9c479b6bd5a5d5f9aae98342fe","impliedFormat":1},{"version":"f4749c102ced952aa6f40f0b579865429c4869f6d83df91000e98005476bee87","impliedFormat":1},{"version":"56654d2c5923598384e71cb808fac2818ca3f07dd23bb018988a39d5e64f268b","impliedFormat":1},{"version":"8b6719d3b9e65863da5390cb26994602c10a315aa16e7d70778a63fee6c4c079","impliedFormat":1},{"version":"05f56cd4b929977d18df8f3d08a4c929a2592ef5af083e79974b20a063f30940","impliedFormat":1},{"version":"547d3c406a21b30e2b78629ecc0b2ddaf652d9e0bdb2d59ceebce5612906df33","impliedFormat":1},{"version":"b3a4f9385279443c3a5568ec914a9492b59a723386161fd5ef0619d9f8982f97","impliedFormat":1},{"version":"3fe66aba4fbe0c3ba196a4f9ed2a776fe99dc4d1567a558fb11693e9fcc4e6ed","impliedFormat":1},{"version":"140eef237c7db06fc5adcb5df434ee21e81ee3a6fd57e1a75b8b3750aa2df2d8","impliedFormat":1},{"version":"0944ec553e4744efae790c68807a461720cff9f3977d4911ac0d918a17c9dd99","impliedFormat":1},{"version":"cb46b38d5e791acaa243bf342b8b5f8491639847463ac965b93896d4fb0af0d9","impliedFormat":1},{"version":"7c7d9e116fe51100ff766703e6b5e4424f51ad8977fe474ddd8d0959aa6de257","impliedFormat":1},{"version":"af70a2567e586be0083df3938b6a6792e6821363d8ef559ad8d721a33a5bcdaf","impliedFormat":1},{"version":"006cff3a8bcb92d77953f49a94cd7d5272fef4ab488b9052ef82b6a1260d870b","impliedFormat":1},{"version":"7d44bfdc8ee5e9af70738ff652c622ae3ad81815e63ab49bdc593d34cb3a68e5","impliedFormat":1},{"version":"339814517abd4dbc7b5f013dfd3b5e37ef0ea914a8bbe65413ecffd668792bc6","impliedFormat":1},{"version":"34d5bc0a6958967ec237c99f980155b5145b76e6eb927c9ffc57d8680326b5d8","impliedFormat":1},{"version":"9eae79b70c9d8288032cbe1b21d0941f6bd4f315e14786b2c1d10bccc634e897","impliedFormat":1},{"version":"18ce015ed308ea469b13b17f99ce53bbb97975855b2a09b86c052eefa4aa013a","impliedFormat":1},{"version":"5a931bc4106194e474be141e0bc1046629510dc95b9a0e4b02a3783847222965","impliedFormat":1},{"version":"5e5f371bf23d5ced2212a5ff56675aefbd0c9b3f4d4fdda1b6123ac6e28f058c","impliedFormat":1},{"version":"907c17ad5a05eecb29b42b36cc8fec6437be27cc4986bb3a218e4f74f606911c","impliedFormat":1},{"version":"ce60a562cd2a92f37a88f2ddd99a3abfbc5848d7baf38c48fb8d3243701fcb75","impliedFormat":1},{"version":"a726ad2d0a98bfffbe8bc1cd2d90b6d831638c0adc750ce73103a471eb9a891c","impliedFormat":1},{"version":"f44c0c8ce58d3dacac016607a1a90e5342d830ea84c48d2e571408087ae55894","impliedFormat":1},{"version":"75a315a098e630e734d9bc932d9841b64b30f7a349a20cf4717bf93044eff113","impliedFormat":1},{"version":"9131d95e32b3d4611d4046a613e022637348f6cebfe68230d4e81b691e4761a1","impliedFormat":1},{"version":"b03aa292cfdcd4edc3af00a7dbd71136dd067ec70a7536b655b82f4dd444e857","impliedFormat":1},{"version":"b6e2b0448ced813b8c207810d96551a26e7d7bb73255eea4b9701698f78846d6","impliedFormat":1},{"version":"8ae10cd85c1bd94d2f2d17c4cbd25c068a4b2471c70c2d96434239f97040747a","impliedFormat":1},{"version":"9ed5b799c50467b0c9f81ddf544b6bcda3e34d92076d6cab183c84511e45c39f","impliedFormat":1},{"version":"b4fa87cc1833839e51c49f20de71230e259c15b2c9c3e89e4814acc1d1ef10de","impliedFormat":1},{"version":"e90ac9e4ac0326faa1bc39f37af38ace0f9d4a655cd6d147713c653139cf4928","impliedFormat":1},{"version":"ea27110249d12e072956473a86fd1965df8e1be985f3b686b4e277afefdde584","impliedFormat":1},{"version":"8776a368617ce51129b74db7d55c3373dadcce5d0701e61d106e99998922a239","impliedFormat":1},{"version":"5666075052877fe2fdddd5b16de03168076cf0f03fbca5c1d4a3b8f43cba570c","impliedFormat":1},{"version":"9108ab5af05418f599ab48186193b1b07034c79a4a212a7f73535903ba4ca249","impliedFormat":1},{"version":"bb4e2cdcadf9c9e6ee2820af23cee6582d47c9c9c13b0dca1baaffe01fbbcb5f","impliedFormat":1},{"version":"6e30d0b5a1441d831d19fe02300ab3d83726abd5141cbcc0e2993fa0efd33db4","impliedFormat":1},{"version":"423f28126b2fc8d8d6fa558035309000a1297ed24473c595b7dec52e5c7ebae5","impliedFormat":1},{"version":"fb30734f82083d4790775dae393cd004924ebcbfde49849d9430bf0f0229dd16","impliedFormat":1},{"version":"2c92b04a7a4a1cd9501e1be338bf435738964130fb2ad5bd6c339ee41224ac4c","impliedFormat":1},{"version":"c5c5f0157b41833180419dacfbd2bcce78fb1a51c136bd4bcba5249864d8b9b5","impliedFormat":1},{"version":"02ae43d5bae42efcd5a00d3923e764895ce056bca005a9f4e623aa6b4797c8af","impliedFormat":1},{"version":"db6e01f17012a9d7b610ae764f94a1af850f5d98c9c826ad61747dca0fb800bd","impliedFormat":1},{"version":"8a44b424edee7bb17dc35a558cc15f92555f14a0441205613e0e50452ab3a602","impliedFormat":1},{"version":"24a00d0f98b799e6f628373249ece352b328089c3383b5606214357e9107e7d5","impliedFormat":1},{"version":"33637e3bc64edd2075d4071c55d60b32bdb0d243652977c66c964021b6fc8066","impliedFormat":1},{"version":"0f0ad9f14dedfdca37260931fac1edf0f6b951c629e84027255512f06a6ebc4c","impliedFormat":1},{"version":"16ad86c48bf950f5a480dc812b64225ca4a071827d3d18ffc5ec1ae176399e36","impliedFormat":1},{"version":"8cbf55a11ff59fd2b8e39a4aa08e25c5ddce46e3af0ed71fb51610607a13c505","impliedFormat":1},{"version":"d5bc4544938741f5daf8f3a339bfbf0d880da9e89e79f44a6383aaf056fe0159","impliedFormat":1},{"version":"97f9169882d393e6f303f570168ca86b5fe9aab556e9a43672dae7e6bb8e6495","impliedFormat":1},{"version":"7c9adb3fcd7851497818120b7e151465406e711d6a596a71b807f3a17853cb58","impliedFormat":1},{"version":"6752d402f9282dd6f6317c8c048aaaac27295739a166eed27e00391b358fed9a","impliedFormat":1},{"version":"9fd7466b77020847dbc9d2165829796bf7ea00895b2520ff3752ffdcff53564b","impliedFormat":1},{"version":"fbfc12d54a4488c2eb166ed63bab0fb34413e97069af273210cf39da5280c8d6","impliedFormat":1},{"version":"85a84240002b7cf577cec637167f0383409d086e3c4443852ca248fc6e16711e","impliedFormat":1},{"version":"84794e3abd045880e0fadcf062b648faf982aa80cfc56d28d80120e298178626","impliedFormat":1},{"version":"053d8b827286a16a669a36ffc8ccc8acdf8cc154c096610aa12348b8c493c7b8","impliedFormat":1},{"version":"3cce4ce031710970fe12d4f7834375f5fd455aa129af4c11eb787935923ff551","impliedFormat":1},{"version":"8f62cbd3afbd6a07bb8c934294b6bfbe437021b89e53a4da7de2648ecfc7af25","impliedFormat":1},{"version":"62c3621d34fb2567c17a2c4b89914ebefbfbd1b1b875b070391a7d4f722e55dc","impliedFormat":1},{"version":"c05ac811542e0b59cb9c2e8f60e983461f0b0e39cea93e320fad447ff8e474f3","impliedFormat":1},{"version":"8e7a5b8f867b99cc8763c0b024068fb58e09f7da2c4810c12833e1ca6eb11c4f","impliedFormat":1},{"version":"132351cbd8437a463757d3510258d0fa98fd3ebef336f56d6f359cf3e177a3ce","impliedFormat":1},{"version":"df877050b04c29b9f8409aa10278d586825f511f0841d1ec41b6554f8362092b","impliedFormat":1},{"version":"33d1888c3c27d3180b7fd20bac84e97ecad94b49830d5dd306f9e770213027d1","impliedFormat":1},{"version":"ee942c58036a0de88505ffd7c129f86125b783888288c2389330168677d6347f","impliedFormat":1},{"version":"a3f317d500c30ea56d41501632cdcc376dae6d24770563a5e59c039e1c2a08ec","impliedFormat":1},{"version":"eb21ddc3a8136a12e69176531197def71dc28ffaf357b74d4bf83407bd845991","impliedFormat":1},{"version":"0c1651a159995dfa784c57b4ea9944f16bdf8d924ed2d8b3db5c25d25749a343","impliedFormat":1},{"version":"aaa13958e03409d72e179b5d7f6ec5c6cc666b7be14773ae7b6b5ee4921e52db","impliedFormat":1},{"version":"0a86e049843ad02977a94bb9cdfec287a6c5a0a4b6b5391a6648b1a122072c5a","impliedFormat":1},{"version":"40f06693e2e3e58526b713c937895c02e113552dc8ba81ecd49cdd9596567ddb","impliedFormat":1},{"version":"4ed5e1992aedb174fb8f5aa8796aa6d4dcb8bd819b4af1b162a222b680a37fa0","impliedFormat":1},{"version":"d7f4bd46a8b97232ea6f8c28012b8d2b995e55e729d11405f159d3e00c51420a","impliedFormat":1},{"version":"d604d413aff031f4bfbdae1560e54ebf503d374464d76d50a2c6ded4df525712","impliedFormat":1},{"version":"e4f4f9cf1e3ac9fd91ada072e4d428ecbf0aa6dc57138fb797b8a0ca3a1d521c","impliedFormat":1},{"version":"12bfd290936824373edda13f48a4094adee93239b9a73432db603127881a300d","impliedFormat":1},{"version":"340ceb3ea308f8e98264988a663640e567c553b8d6dc7d5e43a8f3b64f780374","impliedFormat":1},{"version":"c5a769564e530fba3ec696d0a5cff1709b9095a0bdf5b0826d940d2fc9786413","impliedFormat":1},{"version":"7124ef724c3fc833a17896f2d994c368230a8d4b235baed39aa8037db31de54f","impliedFormat":1},{"version":"5de1c0759a76e7710f76899dcae601386424eab11fb2efaf190f2b0f09c3d3d3","impliedFormat":1},{"version":"9c5ee8f7e581f045b6be979f062a61bf076d362bf89c7f966b993a23424e8b0d","impliedFormat":1},{"version":"1a11df987948a86aa1ec4867907c59bdf431f13ed2270444bf47f788a5c7f92d","impliedFormat":1},{"version":"8018dd2e95e7ce6e613ddd81672a54532614dc745520a2f9e3860ff7fb1be0ca","impliedFormat":1},{"version":"b756781cd40d465da57d1fc6a442c34ae61fe8c802d752aace24f6a43fedacee","impliedFormat":1},{"version":"0fe76167c87289ea094e01616dcbab795c11b56bad23e1ef8aba9aa37e93432a","impliedFormat":1},{"version":"3a45029dba46b1f091e8dc4d784e7be970e209cd7d4ff02bd15270a98a9ba24b","impliedFormat":1},{"version":"032c1581f921f8874cf42966f27fd04afcabbb7878fa708a8251cac5415a2a06","impliedFormat":1},{"version":"69c68ed9652842ce4b8e495d63d2cd425862104c9fb7661f72e7aa8a9ef836f8","impliedFormat":1},{"version":"0e704ee6e9fd8b6a5a7167886f4d8915f4bc22ed79f19cb7b32bd28458f50643","impliedFormat":1},{"version":"06f62a14599a68bcde148d1efd60c2e52e8fa540cc7dcfa4477af132bb3de271","impliedFormat":1},{"version":"904a96f84b1bcee9a7f0f258d17f8692e6652a0390566515fe6741a5c6db8c1c","impliedFormat":1},{"version":"11f19ce32d21222419cecab448fa335017ebebf4f9e5457c4fa9df42fa2dcca7","impliedFormat":1},{"version":"2e8ee2cbb5e9159764e2189cf5547aebd0e6b0d9a64d479397bb051cd1991744","impliedFormat":1},{"version":"1b0471d75f5adb7f545c1a97c02a0f825851b95fe6e069ac6ecaa461b8bb321d","impliedFormat":1},{"version":"1d157c31a02b1e5cca9bc495b3d8d39f4b42b409da79f863fb953fbe3c7d4884","impliedFormat":1},{"version":"07baaceaec03d88a4b78cb0651b25f1ae0322ac1aa0b555ae3749a79a41cba86","impliedFormat":1},{"version":"619a132f634b4ebe5b4b4179ea5870f62f2cb09916a25957bff17b408de8b56d","impliedFormat":1},{"version":"f60fa446a397eb1aead9c4e568faf2df8068b4d0306ebc075fb4be16ed26b741","impliedFormat":1},{"version":"f3cb784be4d9e91f966a0b5052a098d9b53b0af0d341f690585b0cc05c6ca412","impliedFormat":1},{"version":"350f63439f8fe2e06c97368ddc7fb6d6c676d54f59520966f7dbbe6a4586014e","impliedFormat":1},{"version":"eba613b9b357ac8c50a925fa31dc7e65ff3b95a07efbaa684b624f143d8d34ba","impliedFormat":1},{"version":"45b74185005ed45bec3f07cac6e4d68eaf02ead9ff5a66721679fb28020e5e7c","impliedFormat":1},{"version":"0f6199602df09bdb12b95b5434f5d7474b1490d2cd8cc036364ab3ba6fd24263","impliedFormat":1},{"version":"c8ca7fd9ec7a3ec82185bfc8213e4a7f63ae748fd6fced931741d23ef4ea3c0f","impliedFormat":1},{"version":"5c6a8a3c2a8d059f0592d4eab59b062210a1c871117968b10797dee36d991ef7","impliedFormat":1},{"version":"ad77fd25ece8e09247040826a777dc181f974d28257c9cd5acb4921b51967bd8","impliedFormat":1},{"version":"795a08ae4e193f345073b49f68826ab6a9b280400b440906e4ec5c237ae777e6","impliedFormat":1},{"version":"8153df63cf65122809db17128e5918f59d6bb43a371b5218f4430c4585f64085","impliedFormat":1},{"version":"a8150bc382dd12ce58e00764d2366e1d59a590288ee3123af8a4a2cb4ef7f9df","impliedFormat":1},{"version":"5adfaf2f9f33957264ad199a186456a4676b2724ed700fc313ff945d03372169","impliedFormat":1},{"version":"d5c41a741cd408c34cb91f84468f70e9bda3dfeabf33251a61039b3cdb8b22d8","impliedFormat":1},{"version":"a20c3e0fe86a1d8fc500a0e9afec9a872ad3ab5b746ceb3dd7118c6d2bff4328","impliedFormat":1},{"version":"cbaf4a4aa8a8c02aa681c5870d5c69127974de29b7e01df570edec391a417959","impliedFormat":1},{"version":"c7135e329a18b0e712378d5c7bc2faec6f5ab0e955ea0002250f9e232af8b3e4","impliedFormat":1},{"version":"340a45cd77b41d8a6deda248167fa23d3dc67ec798d411bd282f7b3d555b1695","impliedFormat":1},{"version":"fae330f86bc10db6841b310f32367aaa6f553036a3afc426e0389ddc5566cd74","impliedFormat":1},{"version":"2bee1efe53481e93bb8b31736caba17353e7bb6fc04520bd312f4e344afd92f9","impliedFormat":1},{"version":"357b67529139e293a0814cb5b980c3487717c6fbf7c30934d67bc42dad316871","impliedFormat":1},{"version":"99d99a765426accf8133737843fb024a154dc6545fc0ffbba968a7c0b848959d","impliedFormat":1},{"version":"c782c5fd5fa5491c827ecade05c3af3351201dd1c7e77e06711c8029b7a9ee4d","impliedFormat":1},{"version":"883d2104e448bb351c49dd9689a7e8117b480b614b2622732655cef03021bf6d","impliedFormat":1},{"version":"d9b00ee2eca9b149663fdba1c1956331841ae296ee03eaaff6c5becbc0ff1ea8","impliedFormat":1},{"version":"09a7e04beb0547c43270b327c067c85a4e2154372417390731dfe092c4350998","impliedFormat":1},{"version":"eee530aaa93e9ec362e3941ee8355e2d073c7b21d88c2af4713e3d701dab8fef","impliedFormat":1},{"version":"28d47319b97dbeee9130b78eae03b2061d46dedbf92b0d9de13ed7ab8399ccd0","impliedFormat":1},{"version":"6559a36671052ca93cab9a289279a6cef6f9d1a72c34c34546a8848274a9c66c","impliedFormat":1},{"version":"7a0e4cd92545ad03910fd019ae9838718643bd4dde39881c745f236914901dfa","impliedFormat":1},{"version":"c99ebd20316217e349004ee1a0bc74d32d041fb6864093f10f31984c737b8cad","impliedFormat":1},{"version":"6f622e7f054f5ab86258362ac0a64a2d6a27f1e88732d6f5f052f422e08a70e7","impliedFormat":1},{"version":"d62d2ef93ceeb41cf9dfab25989a1e5f9ca5160741aac7f1453c69a6c14c69be","impliedFormat":1},{"version":"1491e80d72873fc586605283f2d9056ee59b166333a769e64378240df130d1c9","impliedFormat":1},{"version":"c32c073d389cfaa3b3e562423e16c2e6d26b8edebbb7d73ccffff4aa66f2171d","impliedFormat":1},{"version":"eca72bf229eecadb63e758613c62fab13815879053539a22477d83a48a21cd73","impliedFormat":1},{"version":"633db46fd1765736409a4767bfc670861468dde60dbb9a501fba4c1b72f8644d","impliedFormat":1},{"version":"f379412f2c0dddd193ff66dcdd9d9cc169162e441d86804c98c84423f993aa8a","impliedFormat":1},{"version":"f2ee748883723aa9325e5d7f30fce424f6a786706e1b91a5a55237c78ee89c4a","impliedFormat":1},{"version":"eda4760e5d7b171132265e970b67c322bcfffacb84248f44def26ed160eb722e","impliedFormat":1},{"version":"142f5190d730259339be1433931c0eb31ae7c7806f4e325f8a470bd9221b6533","impliedFormat":1},{"version":"cbd19f594f0ee7beffeb37dc0367af3908815acf4ce46d86b0515478718cfed8","impliedFormat":1},{"version":"3cdb96f128133efd129c798ac11f959e59d278ae439f69983224774d79ed11db","impliedFormat":1},{"version":"8776e64e6165838ac152fa949456732755b0976d1867ae5534ce248f0ccd7f41","impliedFormat":1},{"version":"896bbc7402b3a403cda96813c8ea595470ff76d31f32869d053317c00ca2589a","impliedFormat":1},{"version":"5c4c5b49bbb01828402bb04af1d71673b18852c11b7e95bfd5cf4c3d80d352c8","impliedFormat":1},{"version":"7030df3d920343df00324df59dc93a959a33e0f4940af3fefef8c07b7ee329bf","impliedFormat":1},{"version":"a96bc00e0c356e29e620eaec24a56d6dd7f4e304feefcc99066a1141c6fe05a7","impliedFormat":1},{"version":"d12cc0e5b09943c4cd0848f787eb9d07bf78b60798e4588c50582db9d4decc70","impliedFormat":1},{"version":"7333ee6354964fd396297958e52e5bf62179aa2c88ca0a35c6d3a668293b7e0e","impliedFormat":1},{"version":"19c3760af3cbc9da99d5b7763b9e33aaf8d018bc2ed843287b7ff4343adf4634","impliedFormat":1},{"version":"9d1e38aeb76084848d2fcd39b458ec88246de028c0f3f448b304b15d764b23d2","impliedFormat":1},{"version":"d406da1eccf18cec56fd29730c24af69758fe3ff49c4f94335e797119cbc0554","impliedFormat":1},{"version":"4898c93890a136da9156c75acd1a80a941a961b3032a0cf14e1fa09a764448b7","impliedFormat":1},{"version":"f5d7a845e3e1c6c27351ea5f358073d0b0681537a2da6201fab254aa434121d3","impliedFormat":1},{"version":"3a47d4582ef0697cccf1f3d03b620002f03fb0ff098f630e284433c417d6c61b","impliedFormat":1},{"version":"d7c30f0abfe9e197e376b016086cf66b2ffb84015139963f37301ed0da9d3d0d","impliedFormat":1},{"version":"ff75bba0148f07775bcb54bf4823421ed4ebdb751b3bf79cc003bd22e49d7d73","impliedFormat":1},{"version":"d40d20ac633703a7333770bfd60360126fc3302d5392d237bbb76e8c529a4f95","impliedFormat":1},{"version":"35a9867207c488061fb4f6fe4715802fbc164b4400018d2fa0149ad02db9a61c","impliedFormat":1},{"version":"b5fd805b7c578ca6a42c42bbfa6fda95a85d9e332106d810bb18116dc13a45f8","impliedFormat":1},{"version":"3abd9ab4fb3a035c865e6a68cb9f4260515354d5ebebacd5c681aee52c046d1f","impliedFormat":1},{"version":"13e82862532619a727cff9a9ba78df7ca66e8a9b69e4cbd18e9809257b6bf7ba","impliedFormat":1},{"version":"601fe4e366b99181cd0244d96418cffeaaa987a7e310c6f0ed0f06ce63dfe3e9","impliedFormat":1},{"version":"c66a4f2b1362abc4aeee0870c697691618b423c8c6e75624a40ef14a06f787b7","impliedFormat":1},{"version":"8808b1c4f84f2e43da98757a959fe7282cb1795737e16534a97b7d4d33e84dfc","impliedFormat":1},{"version":"cd0565ace87a2d7802bf4c20ea23a997c54e598b9eb89f9c75e69478c1f7a0b4","impliedFormat":1},{"version":"738020d2c8fc9df92d5dee4b682d35a776eaedfe2166d12bc8f186e1ea57cc52","impliedFormat":1},{"version":"86dd7c5657a0b0bc6bee8002edcfd544458d3d3c60974555746eb9b2583dc35e","impliedFormat":1},{"version":"d97b96b6ecd4ee03f9f1170722c825ef778430a6a0d7aab03b8929012bf773cd","impliedFormat":1},{"version":"e84e9b89251a57da26a339e75f4014f52e8ef59b77c2ee1e0171cde18d17b3b8","impliedFormat":1},{"version":"272dbfe04cfa965d6fff63fdaba415c1b5a515b1881ae265148f8a84ddeb318f","impliedFormat":1},{"version":"2035fb009b5fafa9a4f4e3b3fdb06d9225b89f2cbbf17a5b62413bf72cea721a","impliedFormat":1},{"version":"eefafec7c059f07b885b79b327d381c9a560e82b439793de597441a4e68d774a","impliedFormat":1},{"version":"72636f59b635c378dc9ea5246b9b3517b1214e340e468e54cb80126353053b2e","impliedFormat":1},{"version":"ebb79f267a3bf2de5f8edc1995c5d31777b539935fab8b7d863e8efb06c8e9ea","impliedFormat":1},{"version":"ada033e6a4c7f4e147e6d76bb881069dc66750619f8cc2472d65beeec1100145","impliedFormat":1},{"version":"0c04cc14a807a5dc0e3752d18a3b2655a135fefbf76ddcdabd0c5df037530d41","impliedFormat":1},{"version":"605d29d619180fbec287d1701e8b1f51f2d16747ec308d20aba3e9a0dac43a0f","impliedFormat":1},{"version":"67c19848b442d77c767414084fc571ce118b08301c4ddff904889d318f3a3363","impliedFormat":1},{"version":"c704ff0e0cb86d1b791767a88af21dadfee259180720a14c12baee668d0eb8fb","impliedFormat":1},{"version":"195c50e15d5b3ea034e01fbdca6f8ad4b35ad47463805bb0360bdffd6fce3009","impliedFormat":1},{"version":"da665f00b6877ae4adb39cd548257f487a76e3d99e006a702a4f38b4b39431cb","impliedFormat":1},{"version":"083aebdd7c96aee90b71ec970f81c48984d9c8ab863e7d30084f048ddcc9d6af","impliedFormat":1},{"version":"9324b243d75bd794f7eaf0704161628dac1f536375b82ac9bebd6e5283a985ba","impliedFormat":1},{"version":"d7a4309673b06223537bc9544b1a5fe9425628e1c8ab5605f3c5ebc27ecb8074","impliedFormat":1},{"version":"0be3da88f06100e2291681bbda2592816dd804004f0972296b20725138ebcddf","impliedFormat":1},{"version":"3eadfd083d40777b403f4f4eecfa40f93876f2a01779157cc114b2565a7afb51","impliedFormat":1},{"version":"cb6789ce3eba018d5a7996ccbf50e27541d850e9b4ee97fdcb3cbd8c5093691f","impliedFormat":1},{"version":"a3684ea9719122f9477902acd08cd363a6f3cff6d493df89d4dc12fa58204e27","impliedFormat":1},{"version":"ff3c48a17bf10dfbb62448152042e4a48a56c9972059997ab9e7ed03b191809b","impliedFormat":1},{"version":"bc3561e460de5a2c19123f618fc1d5a96a484d168884d00666997d847f502bf9","impliedFormat":1},{"version":"c0c46113b4cd5ec9e7cf56e6dbfb3930ef6cbba914c0883eeced396988ae8320","impliedFormat":1},{"version":"118ea3f4e7b9c12e92551be0766706f57a411b4f18a1b4762cfde3cd6d4f0a96","impliedFormat":1},{"version":"01acd7f315e2493395292d9a02841f3b0300e77ccf42f84f4f11460e7623107d","impliedFormat":1},{"version":"656d1ce5b8fbed896bb803d849d6157242261030967b821d01e72264774cab55","impliedFormat":1},{"version":"da66c1b41d833858fe61947432130d39649f0b53d992dfd7d00f0bbe57191ef4","impliedFormat":1},{"version":"835739c6dcf0a9a1533d1e95b7d7cf8e44ca1341652856b897f4573078b23a31","impliedFormat":1},{"version":"774a3bcc0700036313c57a079e2e1161a506836d736203aa0463efa7b11a7e54","impliedFormat":1},{"version":"96577e3f8e0f9ea07ddf748d72dc1908581ef2aafd4ae7418a4574c26027cf02","impliedFormat":1},{"version":"f55971cb3ede99c17443b03788fe27b259dcd0f890ac31badcb74e3ffb4bb371","impliedFormat":1},{"version":"0ef0c246f8f255a5d798727c40d6d2231d2b0ebda5b1ec75e80eadb02022c548","impliedFormat":1},{"version":"ea127752a5ec75f2ac6ef7f1440634e6ae5bc8d09e6f98b61a8fb600def6a861","impliedFormat":1},{"version":"862320e775649dcca8915f8886865e9c6d8affc1e70ed4b97199f3b70a843b47","impliedFormat":1},{"version":"561764374e9f37cb895263d5c8380885972d75d09d0db64c12e0cb10ba90ae3e","impliedFormat":1},{"version":"ee889da857c29fa7375ad500926748ef2e029a6645d7c080e57769923d15dfef","impliedFormat":1},{"version":"56984ba2d781bd742b6bc0fa34c10df2eae59b42ec8b1b731d297f1590fa4071","impliedFormat":1},{"version":"7521de5e64e2dd022be87fce69d956a52d4425286fbc5697ecfec386da896d7e","impliedFormat":1},{"version":"f50b072ec1f4839b54fd1269a4fa7b03efbc9c59940224c7939632c0f70a39c3","impliedFormat":1},{"version":"a5b7ec6f1ff3f1d19a2547f7e1a50ab1284e6b4755d260a481ea01ed2c7cec60","impliedFormat":1},{"version":"1747f9eebf5beb8cfc46cf0303e300950b7bff20cff60b9c46818caced3226e3","impliedFormat":1},{"version":"9d969f36abb62139a90345ee5d03f1c2479831bd84c8f843d87ec304cad96ead","impliedFormat":1},{"version":"e972b52218fd5919aec6cd0e5e2a5fb75f5d2234cf05597a9441837a382b2b29","impliedFormat":1},{"version":"d1e292b0837d0ef5ede4f52363c9d8e93f5d5234086adc796e11eae390305b36","impliedFormat":1},{"version":"0a9e10028a96865d0f25aeca9e3b1ff0691b9b662aa186d9d490728434cf8261","impliedFormat":1},{"version":"1aed740b674839c89f427f48737bad435ee5a39d80b5929f9dc9cc9ac10a7700","impliedFormat":1},{"version":"6e9e3690dc3a6e99a845482e33ee78915893f2d0d579a55b6a0e9b4c44193371","impliedFormat":1},{"version":"4e7a76cce3b537b6cdb1c4b97e29cb4048ee8e7d829cf3a85f4527e92eb573f2","impliedFormat":1},{"version":"7e7e30f804f94b72d23a606f1d281de404a510984085fea8cbbefc7bdcaf1a37","impliedFormat":1},{"version":"46f1fe93f199a419172d7480407d9572064b54712b69406efa97e0244008b24e","impliedFormat":1},{"version":"044e6aaa3f612833fb80e323c65e9d816c3148b397e93630663cda5c2d8f4de1","impliedFormat":1},{"version":"deaf8eb392c46ea2c88553d3cc38d46cfd5ee498238dbc466e3f5be63ae0f651","impliedFormat":1},{"version":"6a79b61f57699de0a381c8a13f4c4bcd120556bfab0b4576994b6917cb62948b","impliedFormat":1},{"version":"c5133d7bdec65f465df12f0b507fbc0d96c78bfa5a012b0eb322cf1ff654e733","impliedFormat":1},{"version":"7905c052681cbe9286797ec036942618e1e8d698dcc2e60f4fb7a0013d470442","impliedFormat":1},{"version":"89049878a456b5e0870bb50289ea8ece28a2abd0255301a261fa8ab6a3e9a07d","impliedFormat":1},{"version":"d0da4f4fd66f37c13deabc1a641edd629141c333ccf862733788bd27e89436ac","impliedFormat":1},{"version":"d4a4f10062a6d82ba60d3ffde9154ef24b1baf2ce28c6439f5bdfb97aa0d18fc","impliedFormat":1},{"version":"f13310c360ecffddb3858dcb33a7619665369d465f55e7386c31d45dfc3847bf","impliedFormat":1},{"version":"e7bde95a05a0564ee1450bc9a53797b0ac7944bf24d87d6f645baca3aa60df48","impliedFormat":1},{"version":"62e68ce120914431a7d34232d3eca643a7ddd67584387936a5202ae1c4dd9a1b","impliedFormat":1},{"version":"91d695bba902cc2eda7edc076cd17c5c9340f7bb254597deb6679e343effadbb","impliedFormat":1},{"version":"e1cb8168c7e0bd4857a66558fe7fe6c66d08432a0a943c51bacdac83773d5745","impliedFormat":1},{"version":"a464510505f31a356e9833963d89ce39f37a098715fc2863e533255af4410525","impliedFormat":1},{"version":"0612b149cabbc136cb25de9daf062659f306b67793edc5e39755c51c724e2949","impliedFormat":1},{"version":"2579b150b86b5f644d86a6d58f17e3b801772c78866c34d41f86f3fc9eb523fe","impliedFormat":1},{"version":"e4b3a3e1b21a194b29d35488ec880948fc2ef8e937288463ea2981ad62a7b106","impliedFormat":1},{"version":"0353e05b0d8475c10ddd88056e0483b191aa5cdea00a25e0505b96e023f1a2d9","impliedFormat":1},{"version":"6a312caabb43c284a4b0da60d5c24f285338096eb9e977af1faca38d32a34685","impliedFormat":1},{"version":"b6eda93163beb978dd0d3042b11c60373506400c94613c0b40d1c0a9a9f1020e","impliedFormat":1},{"version":"a8af4739274959d70f7da4bfdd64f71cfc08d825c2d5d3561bc7baed760b33ef","impliedFormat":1},{"version":"99193bafaa9ce112889698de25c4b8c80b1209bb7402189aea1c7ada708a8a54","impliedFormat":1},{"version":"70473538c6eb9494d53bf1539fe69df68d87c348743d8f7244dcb02ca3619484","impliedFormat":1},{"version":"c48932ab06a4e7531bdca7b0f739ace5fa273f9a1b9009bcd26902f8c0b851f0","impliedFormat":1},{"version":"df6c83e574308f6540c19e3409370482a7d8f448d56c65790b4ac0ab6f6fedd8","impliedFormat":1},{"version":"ebbe6765a836bfa7f03181bc433c8984ca29626270ca1e240c009851222cb8a7","impliedFormat":1},{"version":"20f630766b73752f9d74aab6f4367dba9664e8122ea2edcb00168e4f8b667627","impliedFormat":1},{"version":"468df9d24a6e2bc6b4351417e3b5b4c2ca08264d6d5045fe18eb42e7996e58b4","impliedFormat":1},{"version":"954523d1f4856180cbf79b35bd754e14d3b2aea06c7efd71b254c745976086e9","impliedFormat":1},{"version":"31a030f1225ab463dd0189a11706f0eb413429510a7490192a170114b2af8697","impliedFormat":1},{"version":"6f48f244cd4b5b7e9a0326c74f480b179432397580504726de7c3c65d6304b36","impliedFormat":1},{"version":"5520e6defac8e6cdced6dd28808fafe795cb2cd87407bb1012e13a2b061f50b7","impliedFormat":1},{"version":"c3451661fb058f4e15971bbed29061dd960d02d9f8db1038e08b90d294a05c68","impliedFormat":1},{"version":"1f21aefa51f03629582568f97c20ef138febe32391012828e2a0149c2c393f62","impliedFormat":1},{"version":"b18141cda681d82b2693aef045107a910b90a7409ecff0830e1283f0bb2a53e6","impliedFormat":1},{"version":"18eb53924f27af2a5e9734dce28cf5985df7b2828dade1239241e95b639e9bf1","impliedFormat":1},{"version":"a9f1c52f4e7c2a2c4988b5638bd3dbfe38e408b358d02dd2fb8c8920e877f088","impliedFormat":1},{"version":"a7e10a8ad6536dd0225029e46108b18cee0d3c15c2f6e49bd62798ad85bc57b6","impliedFormat":1},{"version":"8db1ed144dd2304b9bd6e41211e22bad5f4ab1d8006e6ac127b29599f4b36083","impliedFormat":1},{"version":"843a5e3737f2abbbbd43bf2014b70f1c69a80530814a27ae1f8be213ae9ec222","impliedFormat":1},{"version":"6fc1be224ad6b3f3ec11535820def2d21636a47205c2c9de32238ba1ac8d82e6","impliedFormat":1},{"version":"5a44788293f9165116c9c183be66cefef0dc5d718782a04847de53bf664f3cc1","impliedFormat":1},{"version":"afd653ae63ce07075b018ba5ce8f4e977b6055c81cc65998410b904b94003c0a","impliedFormat":1},{"version":"9172155acfeb17b9d75f65b84f36cb3eb0ff3cd763db3f0d1ad5f6d10d55662f","impliedFormat":1},{"version":"71807b208e5f15feffb3ff530bec5b46b1217af0d8cc96dde00d549353bcb864","impliedFormat":1},{"version":"1a6eca5c2bc446481046c01a54553c3ffb856f81607a074f9f0256c59dd0ab13","impliedFormat":1},{"version":"dff93e0997c4e64ff29e9f70cad172c0b438c4f58c119f17a51c94d48164475a","impliedFormat":1},{"version":"fd1ddf926b323dfa439be49c1d41bbe233fe5656975a11183aeb3bf2addfa3bb","impliedFormat":1},{"version":"6dda11db28da6bcc7ff09242cd1866bdddd0ae91e2db3bea03ba66112399641a","impliedFormat":1},{"version":"ea4cd1e72af1aa49cf208b9cb4caf542437beb7a7a5b522f50a5f1b7480362ed","impliedFormat":1},{"version":"903a7d68a222d94da11a5a89449fdd5dd75d83cd95af34c0242e10b85ec33a93","impliedFormat":1},{"version":"e7fe2e7ed5c3a7beff60361632be19a8943e53466b7dd69c34f89faf473206d7","impliedFormat":1},{"version":"b4896cee83379e159f83021e262223354db79e439092e485611163e2082224ff","impliedFormat":1},{"version":"5243e79a643e41d9653011d6c66e95048fc0478eb8593dc079b70877a2e3990e","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"c8b8968311ec4e5e97b7b5fb8a65efaba455db9bdcfd7fff7fb15f6e317bfba0","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aecbf1d9e6a18dab7d92ef8a89a1444b47e1eb6134cb2bb776a26d55ff58c29a","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce697b6a251d9cad53998c7fd3098072df883b525ec45d83530e434dc6d80dc6","impliedFormat":1},{"version":"719412f054e6ecc35489462c9a21bab0323d173a7d04e55b0ace4b5d86fbeb07","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"3db996ecdee7aabecc5385976cc07eb66216034a273c07b17d1a85292e9bab0c","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"c1fa52b3d014001e8662fa2669d90ea15373958a288e3b83a3b621733d25292a","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"47d480d0c84b0f6215f1f817e0939b247ebf5217b6784846b96fa6ab710ce99d","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"8413d0641f293aed551c7464615b770d34a02dedede889b9591172287d68e773","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"1b241e24f3227d078c06aeda6e050187ad59a4e591f4467abed44d92b084e08d","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f36b3fbe2be150a9ca140da48593f21e6a8172004f92ddc549b43efec39f3e54","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"1c4f139ade4f6ebf45463505f8155173e5d7a5305e50e0aae0a5e712d6ff3b48","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"1c9e5b1a17b1fc9b3711fb36e0690421261ab2880f15b145155b5b2ba2ab6c2d","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"6953d7597831d0860c7034cf4f0419687d263b6b98a4b32e37ce6d49615c36e2","impliedFormat":1},{"version":"76e7352249c42b9d54fe1f9e1ebcef777da1cb2eb33038366af49469d433597b","impliedFormat":1},{"version":"88cb622dd0ec1ef860e5c27fa884e60d2eba5ae22c7907dff82c56a69bdd2c8a","impliedFormat":1},{"version":"eb234b3e285e8bc071bdddc1ec0460095e13ead6222d44b02c4e0869522f9ba3","impliedFormat":1},{"version":"c85114872760189e50fef131944427b0fb367f0cc0b6dce164bb427a6fd89381","impliedFormat":1},{"version":"5ad69b0d7e7bdbcd3adfdb6a3e306e935c9c2711b1c60493646504a2f991346e","impliedFormat":1},{"version":"a12a667efdeb03b529bd4ebb4032998ddd32743799f59f9f18b186f8e63a2cf1","impliedFormat":1},{"version":"cee7efa0ae4c58deab218d1df0d1bf84abfd5c356cff28bca1421489cba13a19","impliedFormat":1},{"version":"f9e034b1ae29825c00532e08ea852b0c72885c343ee48d2975db0a6481218ab3","impliedFormat":1},{"version":"1193f49cbb883f40326461fe379e58ffa4c18d15bf6d6a1974ad2894e4fb20f3","impliedFormat":1},{"version":"8f1241f5d9f0d3d72117768b3c974e462840fbd85026fb66685078945404cf2f","impliedFormat":1},{"version":"6ecc423e71318bafbd230e6059e082c377170dfc7e02fccfa600586f8604d452","impliedFormat":1},{"version":"772f9bdd2bf50c9c01b0506001545e9b878faa7394ad6e7d90b49b179a024584","impliedFormat":1},{"version":"46fbbc428aa66412a94e7c1d455b6ae592ee57e0adb080bfa2abacc2a63b21a1","impliedFormat":1},{"version":"df251aa7a87b6d003a45ad1f71b87ff4448e535db473cb3c897720012b5553d2","impliedFormat":1},{"version":"20ccce97c422c215a41f0aa8373eb2b6f4068633ceef401c5f743cecdf8119bf","impliedFormat":1},{"version":"cd1ccdd9fd7980d43dfede5d42ee3d18064baed98b136089cf7c8221d562f058","impliedFormat":1},{"version":"d60f9a4fd1e734e7b79517f02622426ea1000deb7d6549dfdece043353691a4e","impliedFormat":1},{"version":"ec05ccc3a2e35ef2800a5b5ed2eb2ad4cd004955447bebd86883ddf49625b400","impliedFormat":1},{"version":"403d28b5e5f8fcff795ac038902033ec5890143e950af45bd91a3ed231e8b59c","impliedFormat":1},{"version":"0b17ac073f8dd236cb4ec1f16c84ac2632713bf421305c96a536c360b1500f01","impliedFormat":1},{"version":"c73b59f91088c00886d44ca296d53a75c263c3bda31e3b2f37ceb137382282be","impliedFormat":1},{"version":"e7aa2c584edb0970cb4bb01eb10344200286055f9a22bc3dadcc5a1f9199af3e","impliedFormat":1},{"version":"bfeb476eb0049185cb94c2bfcadb3ce1190554bbcf170d2bf7c68ed9bb00458e","impliedFormat":1},{"version":"ae23a65a2b664ffe979b0a2a98842e10bdf3af67a356f14bbc9d77eb3ab13585","impliedFormat":1},{"version":"2db00053dff66774bc4216209acf094dd70d9dfd8211e409fc4bd8d10f7f66f6","impliedFormat":1},{"version":"eccf6ad2a8624329653896e8dbd03f30756cbd902a81b5d3942d6cf0e1a21575","impliedFormat":1},{"version":"1930c964051c04b4b5475702613cd5a27fcc2d33057aa946ff52bfca990dbc84","impliedFormat":1},{"version":"762992adfa3fbf42c0bce86caed3dc185786855b21a20265089770485e6aa9d3","impliedFormat":1},{"version":"8a440978a6b5c6e6ea76a2804a90c06cadbefb96f6680785d8d3bfab8c8875d8","impliedFormat":1},{"version":"62463aa3d299ae0cdc5473d2ac32213a05753c3adce87a8801c6d2b114a64116","impliedFormat":1},{"version":"05a0d93bb8653bb3833c1c22e3fa5432c38885b2e545e99524e67e25e5ce355c","impliedFormat":1},{"version":"fb4c35b52a35353805407ea9fa1baf3ef102e2f8e0d37a88c0fc0099f3df5dbb","impliedFormat":1},{"version":"40abfc1faa2971acedb69bde8d8c4bbd4edce4af12f786e747dfb8298e6a05a1","impliedFormat":1},{"version":"80ae880ac3e366a8aca79089c9d8467b1753e19fb65a9cf43bbfdb6db1ac588d","impliedFormat":1},{"version":"02153ce9d452d116e9b7bfe42058c02c4cac09f49c46450820b4b44c83306a3e","impliedFormat":1},{"version":"f689c0633e8c95f550d36af943d775f3fae3dac81a28714b45c7af0bbb76a980","impliedFormat":1},{"version":"34be0f820d16a54625b86ab6d7b7928dbb819b58ddac181aa3836390c8c3df24","impliedFormat":1},{"version":"0495afa06118083a11cd4da27acfd96a01b989aff0fc633823c5febe9668ef15","impliedFormat":1},{"version":"67feb4436be89f58ba899dec57f6e703bee1bb7205ba21ab50fca237f6753787","impliedFormat":1},{"version":"75849f5ead7684bf85ee9cce7e84683ed4332fa187f8ee0978ba9df96c5cee06","impliedFormat":1},{"version":"b5325ff5c9dc488bb9c87711faf2b73f639c45f190b81df88ed056807206958b","impliedFormat":1},{"version":"3a8c0797ff5e1cfb5438f0c5a22a36500320c075b74e964da1ec1424b5b526f0","impliedFormat":1},{"version":"a743cf98667fdbb6989d9a7629d25a9824a484ce639bbf2740dc809341e6dbce","impliedFormat":1},{"version":"d73a2cbf1893668c1fc0b41878a9df37bbb128cb05e292b845f68b7b88ef9957","impliedFormat":1},{"version":"eaa70e04ed55081afa11e3aa261b14ed26507e1a0b0fbade405c6b50c70b2059","impliedFormat":1},{"version":"eb89de249984a951578b5fa472d0414cd10cd7d35777aa1c7c308dfe2b0b49d6","impliedFormat":1},{"version":"0fe4a1bd28d903694da55eb5955297a5db1fc87720a45ae60b7640bb5310368c","impliedFormat":1},{"version":"a30b92bc54447834c3b4443b388891bf11cce5b25b8edbfc29184d2abd51c991","impliedFormat":1},{"version":"5e168f60c9132665853ccba13a29e4d311ac174c0fd3eda5cee7a6c5071802d6","impliedFormat":1},{"version":"1c225a18846203fafc4334658715b0d3fd3ee842c4cfd42e628a535eda17730d","impliedFormat":1},{"version":"7ce93da38595d1caf57452d57e0733474564c2b290459d34f6e9dcf66e2d8beb","impliedFormat":1},{"version":"d7b672c1c583e9e34ff6df2549d6a55d7ca3adaf72e6a05081ea9ee625dac59f","impliedFormat":1},{"version":"f3a2902e84ebdef6525ed6bf116387a1256ea9ae8eeb36c22f070b7c9ea4cf09","impliedFormat":1},{"version":"788e6ec863183b001b1e96d54f876c9d5436fa3aa19397093912dcdae466caf7","impliedFormat":1},{"version":"ae3e98448468e46474d817b5ebe74db11ab22c2feb60e292d96ce1a4ee963623","impliedFormat":1},{"version":"f5c67304429e2e2554f63526e842f47bd9ee0cb0f18becc963b93b2d3fafe9c2","impliedFormat":1},{"version":"8903c1df475f531bf787bc795c718e415ce9974536d8429619fdc456f5329948","impliedFormat":1},{"version":"4bac2738a569f77e13bff48946a7b8e98db314cc5c8e75d14e7efd6b1ed52a85","impliedFormat":1},{"version":"337de7eeeb4bc95014657067d2c3f370d7b3935c8c18fb5e12d4c00545ee519b","impliedFormat":1},{"version":"c7dc98d27c1b991852351817a238385366a4302111d12b79695df9d4783cae66","signature":"70e810b4f31cf54e9f73a64c73be54b12a2e5dff4d2f7b4e045f96df5e27f06c"},{"version":"617cca499054c26280cf7a274ba61634ca4a2376afb322f72e5af4f2cc2d7689","impliedFormat":1},{"version":"b1a6b13030544850d3032928604dd91e28ca82862346aaebff1049b645daf12f","impliedFormat":1},{"version":"cacdb65cdb2fe683dc28ff4cde421f040e0f277234842849ad29606995c484c7","impliedFormat":1},{"version":"d5eb5865d4cbaa9985cc3cfb920b230cdcf3363f1e70903a08dc4baab80b0ce1","impliedFormat":1},{"version":"51ebca098538b252953b1ef83c165f25b52271bfb6049cd09d197dddd4cd43c5","impliedFormat":1},{"version":"8aa255453712ce8df1652947a0406769415ee7113c54551310e36cf6ab9db99c","impliedFormat":1},{"version":"6d5618deb494f418484f7c41319f2bc8c34bd071720e377df61194f7a51ff518","impliedFormat":1},{"version":"f60e3e3060207ac982da13363181fd7ee4beecc19a7c569f0d6bb034331066c2","impliedFormat":1},{"version":"4a20a0a39aca8738cc5088f9dea2969bf2af3ca1341c56071f513059b122d150","impliedFormat":1},{"version":"a33c98f95c099c2a9a383b156c8f2a16605843d29455a954c1431bcbbc1ce35a","impliedFormat":1},{"version":"effa1fd4980f6dff4e81ed67009b847d34449087504319f9a0dd081e8cf23a31","impliedFormat":1},{"version":"c2c9f431f788b434e6ae327844ef6712bb6a6a88ad08acf20bf6cfeb41d154ba","impliedFormat":1},{"version":"798367363a3274220cbed839b883fe2f52ba7197b25e8cb2ac59c1e1fd8af6b7","impliedFormat":1},{"version":"2636a309ed87d6876728d9aca846a76b372cf2a21a4fdf9940a82a2dd86687d0","impliedFormat":1},{"version":"55917f3d5b339d0a8e57c4898dfbc19d3941643f716f4830187068e799159ea4","impliedFormat":1},"44b9fc7cc9ebe1235a25b027e1205511bc2ee5eb42b7253e6aa07616b20a5dcb","b5d956f435ebe2c610d28a0e57c80576809bfe6150f76ef1fe61db399a758b2d","65419ba0ec4b4f5603c47369261619a48464f23b2696d7be1b85b80a23ee7bd4","dc9fc49c8494b4fd0650800a7767fb7a524b1023bbc8e09082d80eff85c0495f","ab9b782f10ec487834780a7adfde513ffc3c022285661cc891e8e130aa82e2fd","e5761a2870edec6990ed21ade3ddd28e132d65b9dfa371ac63548097d06f6f40","19ff6e335cf5a64247219c3b159f634ee882823897844d348f02572bddca48f1","80b04e75398c6e6e2d1f85933f0e1c5305ef80753f5cd09dc2f7786fb2396db4","0b3b6c03dcde154812048058466b45ca544c8d63957f74d5d16b5f84f1a4f344","a9ff19c64dcd9e378a5966c06bfdcdd2a706c815578f9419bfd664be2e83f57c",{"version":"7f9fbaad92cd7b2e8c534f09188700eb981d9b7ad5356c2fbb3f4ba9263ec63d","signature":"a8d5b89e0a794923f0917bc9bb78bf62a31e9258cd2960727d842c8bcd5cfc41"},{"version":"8462ca91926f2eeda443728f23b6ca457e63ce1a8a79f81c6aa6965129573abb","signature":"8ff153e2079b90beebf1d022d75c716669a53928c6147f414b0db72a8ccc8c2c"},"c4b97bc6a9a4e7525eee7d639ea50c2613211e6400ad380365b957a891b72691","6b973d12a26b7a6a52fe99b63a9c6d0deab643ccc00de0d44553141fdc1a01c7","e7c360acccf6fec1f5a4fca1230a642fad2dcea474dcf207445de74f55cd9850","200736356ab6fb37115cf04b53f0ff69cd2e5bad11e61fcb05a342b1690d3b9f","e0c6fef4f20d121dc1945d8833c6ded1a10179cc7d6bfef019e3c68b6c55aa6d","7ba3b34b7df43376dec5168e5c620ee598a58c9d32ec73cf2be469206ec69c9c","edc486f4a2e915288fbb70fea901c7a72dba3a6e48cf2ce62965e9d936642554",{"version":"6bde3e00980f7ec94aa825e94c5f7a5c4c3c2342a41a8ffd5bc23c750251e58d","impliedFormat":1},{"version":"717b120720ab5a9ed26d119eb1e4024d161a9e4676ec44fb43f95a27d163ec5a","impliedFormat":1},{"version":"67c4e219bccbd87b8e054213e80b1278be8da8b099d45af747dccfb4dfd7af6f","impliedFormat":1},{"version":"c2621930ef221aff59317993158f5d87662eb5bca09d5ce3ae1e41d7187c9384","impliedFormat":1},{"version":"f1a60cadab021078f04200daf4c66ee70d93e5a9123f6fa05ba4128fcf69c0d9","impliedFormat":1},{"version":"27679e96d1bd38c5938178aaf4abe8627493090b63d6bae2ce8436e6a87ebe4d","impliedFormat":1},{"version":"eb967aca6a70476eabb2b2de534f94cf58dd266e7c038d8c54f8d17f990eac96","impliedFormat":1},{"version":"f95c1b0910c4e0432b76a5dbec026aa8b725ac0b107b7bcff20b25ae8101c1bb","impliedFormat":1},{"version":"30ce903610d8fff81c1587d01c033eaaa5f3a9cbdf7daa9da87706cc52f5aa04","impliedFormat":1},{"version":"4c5d2a6d41a3ae593c805120d0eb155036d161eda42cc5d5214b27def43c4ee1","impliedFormat":1},{"version":"dbca9f6a3e220261be9b53a01de40279c841130cc416e284bb1727a56010c43f","impliedFormat":1},{"version":"09beee23a66f0bdf7dc331f4a02e92c2b5394007b64370ed2d27734bc5536491","impliedFormat":1},{"version":"27562a4464551037e4287cf3261d164427754c5bd7c6ad27bf028bec82fc86b2","impliedFormat":1},{"version":"e13b50da243d4d0fd5e2ec6397c1be56a131f0da2c124ffa1fcedd98807f182b","impliedFormat":1},{"version":"35605aa025dccd05aa7bcf114ee4d41f44f6c9d3a7bb7a00aee12b261134e1be","impliedFormat":1},{"version":"e20666467164069e2de47bcf797c128263f9f8082f930c9da4b0f68bbfd4baff","signature":"5a311feb65213de9ee2557fc3b3e32ca88c0040eb5203f63c202cd1c38c2f985"},"2613f1ac1ee710dae817e7a3256b4a2da344bff5fd1e1966ee81ad59dea7d32d","770774f05c16f59ba1311b9e55b5550c255a2a111c92ddd09368779b847e0272","db2e073fa5f0561b8dffd815122cd9236effb16b28477e5a0199749cd4b3dbd9","772e99e4b56d6dec0089bae8cae02dfc45050e5acbccb767ba8c8ad944956e34","9223a0889abb0669020e94a9b8c1e68274cdc05533c1f79d84fe516450e94ebd","1dc5f46dd70bf9d8b5d861e95d71042eb0d74005ce65a26d21e9578f051e50cb","8eab93ca8c261488ab0ef4d99a4700cf0deb4d941a365b959790f80a45b3bef2","514495e5c0eb05caa1e8b567fe38327e2ec65ee8f2c4cae3acb5b23877133d5c","5d9365f103debd67a88706cbafb4d4ed7f7b6d8a2ecef7061443f54df9063054","84afbc01872edd5aab3ea2a9966e959445b9bb64783e91ad5e24bf312953552c","83750ee83f4b628e9ccf8256c628b8369cd28222124b07fc341b4c16924bc211","e878006e2ac2c2d2369b3ed8782ecbd90950bbb7e553685503ad6d9c633e5bb5","4695cc91dc25f6b25c4294dad18e0b1a1060268173c265eff0758d8d34f8d778",{"version":"04de5584b953b03611eeef01ba9948607def8f64f1e7fbc840752b13b4521b52","impliedFormat":1},{"version":"8b0b6a4c032a56d5651f7dd02ba3f05fbfe4131c4095093633cda3cae0991972","impliedFormat":1},{"version":"192a0c215bffe5e4ac7b9ff1e90e94bf4dfdad4f0f69a5ae07fccc36435ebb87","impliedFormat":1},{"version":"3ef8565e3d254583cced37534f161c31e3a8f341ff005c98b582c6d8c9274538","impliedFormat":1},{"version":"d7e42a3800e287d2a1af8479c7dd58c8663e80a01686cb89e0068be6c777d687","impliedFormat":1},{"version":"1098034333d3eb3c1d974435cacba9bd5a625711453412b3a514774fec7ca748","impliedFormat":1},{"version":"f2388b97b898a93d5a864e85627e3af8638695ebfa6d732ecd39d382824f0e63","impliedFormat":1},{"version":"f130eccfce6e5c42ac92e6d070aa2e41c34312b3a75ce8e2c0dd57e5d3cf1d50","impliedFormat":1},{"version":"f477375e6f0bf2a638a71d4e7a3da8885e3a03f3e5350688541d136b10b762a6","impliedFormat":1},{"version":"a44d6ea4dc70c3d789e9cef3cc42b79c78d17d3ce07f5fd278a7e1cbe824da56","impliedFormat":1},{"version":"55cd8cbc22fe648429a787e16a9cd2dc501a2aafd28c00254ad120ef68a581c0","impliedFormat":1},{"version":"ba4900e9d6f9795a72e8f5ca13c18861821a3fc3ae7858acb0a3366091a47afb","impliedFormat":1},{"version":"7778e2cc5f74ef263a880159aa7fa67254d6232e94dd03429a75597a622537a7","impliedFormat":1},{"version":"8e06a1ef49502a62039eeb927a1bd7561b0bce48bd423a929e2e478fd827c273","impliedFormat":1},{"version":"7ec3d0b061da85d6ff50c337e3248a02a72088462739d88f33b9337dba488c4f","impliedFormat":1},{"version":"2f554c6798b731fc39ff4e3d86aadc932fdeaa063e3cbab025623ff5653c0031","impliedFormat":1},{"version":"fe4613c6c0d23edc04cd8585bdd86bc7337dc6265fb52037d11ca19eeb5e5aaf","impliedFormat":1},{"version":"53b26fbee1a21a6403cf4625d0e501a966b9ccf735754b854366cee8984b711c","impliedFormat":1},{"version":"9ff247206ec5dffdfadddfded2c9d9ad5f714821bb56760be40ed89121f192f4","impliedFormat":1},{"version":"a1a42747e6f2d60162b334556d5355a261e194910e6d7b5f1ad2087668b7964b","impliedFormat":1},{"version":"8c59d8256086ed17676139ee43c1155673e357ab956fb9d00711a7cac73e059d","impliedFormat":1},{"version":"cfe88132f67aa055a3f49d59b01585fa8d890f5a66a0a13bb71973d57573eee7","impliedFormat":1},{"version":"53ce488a97f0b50686ade64252f60a1e491591dd7324f017b86d78239bd232ca","impliedFormat":1},{"version":"50fd11b764194f06977c162c37e5a70bcf0d3579bf82dd4de4eee3ac68d0f82f","impliedFormat":1},{"version":"e0ceb647dcdf6b27fd37e8b0406c7eafb8adfc99414837f3c9bfd28ffed6150a","impliedFormat":1},{"version":"99579aa074ed298e7a3d6a47e68f0cd099e92411212d5081ce88344a5b1b528d","impliedFormat":1},{"version":"096e4ddaa8f0aa8b0ceadd6ab13c3fab53e8a0280678c405160341332eca3cd7","impliedFormat":1},{"version":"415b55892d813a74be51742edd777bbced1f1417848627bf71725171b5325133","impliedFormat":1},{"version":"942ab34f62ac3f3d20014615b6442b6dc51815e30a878ebc390dd70e0dec63bf","impliedFormat":1},{"version":"7a671bf8b4ad81b8b8aea76213ca31b8a5de4ba39490fbdee249fc5ba974a622","impliedFormat":1},{"version":"8e07f13fb0f67e12863b096734f004e14c5ebfd34a524ed4c863c80354c25a44","impliedFormat":1},{"version":"9faa56e38ed5637228530065a9bab19a4dc5a326fbdd1c99e73a310cfed4fcde","impliedFormat":1},{"version":"7d4ad85174f559d8e6ed28a5459aebfc0a7b0872f7775ca147c551e7765e3285","impliedFormat":1},{"version":"d422f0c340060a53cb56d0db24dd170e31e236a808130ab106f7ab2c846f1cdb","impliedFormat":1},{"version":"424403ef35c4c97a7f00ea85f4a5e2f088659c731e75dbe0c546137cb64ef8d8","impliedFormat":1},{"version":"16900e9a60518461d7889be8efeca3fe2cbcd3f6ce6dee70fea81dfbf8990a76","impliedFormat":1},{"version":"6daf17b3bd9499bd0cc1733ab227267d48cd0145ed9967c983ccb8f52eb72d6e","impliedFormat":1},{"version":"e4177e6220d0fef2500432c723dbd2eb9a27dcb491344e6b342be58cc1379ec0","impliedFormat":1},{"version":"ddc62031f48165334486ad1943a1e4ed40c15c94335697cb1e1fd19a182e3102","impliedFormat":1},{"version":"b3f4224eb155d7d13eb377ef40baa1f158f4637aa6de6297dfeeacefd6247476","impliedFormat":1},{"version":"4a168e11fe0f46918721d2f6fcdb676333395736371db1c113ae30b6fde9ccd2","impliedFormat":1},{"version":"5b0a75a5cced0bed0d733bde2da0bbb5d8c8c83d3073444ae52df5f16aefb6ab","impliedFormat":1},{"version":"ef2c1585cad462bdf65f2640e7bcd75cd0dbc45bae297e75072e11fe3db017fa","impliedFormat":1},{"version":"ef809928a4085de826f5b0c84175a56d32dd353856f5b9866d78b8419f8ea9bc","impliedFormat":1},{"version":"6f6eadb32844b0ec7b322293b011316486894f110443197c4c9fbcba01b3b2fa","impliedFormat":1},{"version":"a51e08f41e3e948c287268a275bfe652856a10f68ddd2bf3e3aaf5b8cdb9ef85","impliedFormat":1},{"version":"862f7d760ef37f0ae2c17de82e5fbf336b37d5c1b0dcf39dcd5468f90a7fdd54","impliedFormat":1},{"version":"af48a76b75041e2b3e7bd8eed786c07f39ea896bb2ff165e27e18208d09b8bee","impliedFormat":1},{"version":"cb524ec077f3963e13e85747c6b53fbdf6bf407c84ca1873c6e43da1e96bee6d","impliedFormat":1},{"version":"deb092bc337b2cb0a1b14f3d43f56bc663e1447694e6d479d6df8296bdd452d6","impliedFormat":1},{"version":"041bc1c3620322cb6152183857601707ef6626e9d99f736e8780533689fb1bf9","impliedFormat":1},{"version":"22bd7c75de7d68e075975bf1123de5bccecfd06688afff2e2022b4c70bfc91c3","impliedFormat":1},{"version":"128e7c2ffd37aa29e05367400d718b0e4770cefb1e658d8783ec80a16bc0643a","impliedFormat":1},{"version":"076ac4f2d642c473fa7f01c8c1b7b4ef58f921130174d9cf78430651f44c43ec","impliedFormat":1},{"version":"396c1e5a39706999ec8cc582916e05fcb4f901631d2c192c1292e95089a494d9","impliedFormat":1},{"version":"89df75d28f34fc698fe261f9489125b4e5828fbd62d863bbe93373d3ed995056","impliedFormat":1},{"version":"8ccf5843249a042f4553a308816fe8a03aa423e55544637757d0cfa338bb5186","impliedFormat":1},{"version":"93b44aa4a7b27ba57d9e2bad6fb7943956de85c5cc330d2c3e30cd25b4583d44","impliedFormat":1},{"version":"a0c6216075f54cafdfa90412596b165ff85e2cadd319c49557cc8410f487b77c","impliedFormat":1},{"version":"3c359d811ec0097cba00fb2afd844b125a2ddf4cad88afaf864e88c8d3d358bd","impliedFormat":1},{"version":"3c0b38e8bf11bf3ab87b5116ae8e7b2cad0147b1c80f2b77989dea6f0b93e024","impliedFormat":1},{"version":"8df06e1cd5bb3bf31529cc0db74fa2e57f7de1f6042726679eb8bc1f57083a99","impliedFormat":1},{"version":"d62f09256941e92a95b78ae2267e4cf5ff2ca8915d62b9561b1bc85af1baf428","impliedFormat":1},{"version":"e6223b7263dd7a49f4691bf8df2b1e69f764fb46972937e6f9b28538d050b1ba","impliedFormat":1},{"version":"d9b59eb4e79a0f7a144ee837afb3f1afbc4dab031e49666067a2b5be94b36bd4","impliedFormat":1},{"version":"1db014db736a09668e0c0576585174dbcfd6471bb5e2d79f151a241e0d18d66b","impliedFormat":1},{"version":"8a153d30edde9cefd102e5523b5a9673c298fc7cf7af5173ae946cbb8dd48f11","impliedFormat":1},{"version":"abaaf8d606990f505ee5f76d0b45a44df60886a7d470820fcfb2c06eafa99659","impliedFormat":1},{"version":"51a66bfa412057e786a712733107547ceb6f539061f5bf1c6e5a96e4ccf4f83c","impliedFormat":1},{"version":"d92a80c2c05cf974704088f9da904fe5eadc0b3ad49ddd1ef70ca8028b5adda1","impliedFormat":1},{"version":"fbd7450f20b4486c54f8a90486c395b14f76da66ba30a7d83590e199848f0660","impliedFormat":1},{"version":"ece5b0e45c865645ab65880854899a5422a0b76ada7baa49300c76d38a530ee1","impliedFormat":1},{"version":"62d89ac385aeab821e2d55b4f9a23a277d44f33c67fefe4859c17b80fdb397ea","impliedFormat":1},{"version":"f4dee11887c5564886026263c6ee65c0babc971b2b8848d85c35927af25da827","impliedFormat":1},{"version":"fb8dd49a4cd6d802be4554fbab193bb06e2035905779777f32326cb57cf6a2c2","impliedFormat":1},{"version":"e403ecdfba83013b5eb0e648a92ce182bff2a45ccb81db3035a69081563c2830","impliedFormat":1},{"version":"82d3e00d56a71fc169f3cf9ec5f5ffcc92f6c0e67d4dfc130dafe9f1886d5515","impliedFormat":1},{"version":"b8d57effce2d49a5493debbd8c644e8d52fbe66e2c6d451371375ef5f7bccb8e","impliedFormat":1},{"version":"856024d913cc60b17cf63970f2707ed549c562ccf6ccbb14a171df5a4d1ea44f","impliedFormat":1},{"version":"1b33478647aa1b771314745807397002a410c746480e9447db959110999873ce","impliedFormat":1},{"version":"a14e7c48debe27b25ddf7932e6976c4f58123e32be8384c3f91b0a4d9f67c2f0","impliedFormat":1},{"version":"7e6a96b383da9f5acb848bb9dedb9ac8489df7cec46bbf26aeaed2610f709078","impliedFormat":1},{"version":"9fac6ebf3c60ced53dd21def30a679ec225fc3ff4b8d66b86326c285a4eebb5a","impliedFormat":1},{"version":"8cb83cb98c460cd716d2a98b64eb1a07a3a65c7362436550e02f5c2d212871d1","impliedFormat":1},{"version":"07bc8a3551e39e70c38e7293b1a09916867d728043e352b119f951742cb91624","impliedFormat":1},{"version":"e47adc2176f43c617c0ab47f2d9b2bb1706d9e0669bf349a30c3fe09ddd63261","impliedFormat":1},{"version":"7fec79dfd7319fec7456b1b53134edb54c411ba493a0aef350eee75a4f223eeb","impliedFormat":1},{"version":"189c489705bb96a308dcde9b3336011d08bfbca568bcaf5d5d55c05468e9de7a","impliedFormat":1},{"version":"98f4b1074567341764b580bf14c5aabe82a4390d11553780814f7e932970a6f7","impliedFormat":1},{"version":"1dd24cbf39199100fbe2f3dbd1c7203c240c41d95f66301ecc7650ae77875be1","impliedFormat":1},{"version":"2e252235037a2cd8feebfbf74aa460f783e5d423895d13f29a934d7655a1f8be","impliedFormat":1},{"version":"3bd10a31e9066676e0af937c2ef2507451281861ae294d04c7c46e46706140d9","impliedFormat":1},"cd5a165a9d51a46669a414aa5a67250ae74347802ce80198745009bd59cb5432"],"root":[611,[627,645],[661,674],767],"options":{"allowSyntheticDefaultImports":true,"declaration":false,"emitDecoratorMetadata":true,"esModuleInterop":true,"experimentalDecorators":true,"module":1,"outDir":"./dist","removeComments":true,"rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"target":9},"referencedMap":[[615,1],[614,2],[343,3],[330,3],[67,3],[319,4],[320,4],[321,3],[322,5],[332,6],[323,4],[324,7],[325,3],[326,3],[327,4],[328,4],[329,4],[331,8],[339,9],[341,3],[338,3],[345,10],[342,3],[340,3],[336,11],[337,12],[344,13],[346,14],[333,3],[335,15],[334,16],[273,3],[276,17],[272,3],[723,3],[274,3],[275,3],[349,18],[350,18],[351,18],[352,18],[353,18],[354,18],[355,18],[348,19],[356,18],[370,20],[357,18],[347,3],[358,18],[359,18],[360,18],[361,18],[362,18],[363,18],[364,18],[365,18],[366,18],[367,18],[368,18],[369,18],[378,21],[376,22],[375,3],[374,3],[377,23],[418,24],[68,3],[69,3],[70,3],[705,25],[72,26],[711,27],[710,28],[262,29],[263,26],[398,3],[292,3],[293,3],[399,30],[264,3],[400,3],[401,31],[71,3],[266,32],[267,33],[265,34],[268,32],[269,3],[271,35],[283,36],[284,3],[289,37],[285,3],[286,3],[287,3],[288,3],[290,3],[291,38],[297,39],[300,40],[298,3],[299,3],[318,41],[301,3],[302,3],[754,42],[282,43],[280,44],[278,45],[279,46],[281,3],[309,47],[303,3],[312,48],[305,49],[310,50],[308,51],[311,52],[306,53],[307,54],[295,55],[314,56],[296,57],[316,58],[317,59],[304,3],[313,3],[270,3],[277,60],[315,61],[384,62],[379,3],[385,63],[380,64],[381,65],[382,66],[383,67],[386,68],[391,69],[389,70],[390,70],[397,71],[387,3],[388,72],[392,69],[394,73],[396,74],[395,75],[410,76],[403,77],[404,78],[405,78],[406,79],[407,79],[408,78],[409,78],[402,80],[412,81],[411,82],[414,83],[413,84],[415,85],[371,86],[373,87],[294,3],[372,55],[416,88],[393,89],[417,90],[419,5],[555,91],[556,92],[560,93],[420,3],[426,94],[553,95],[554,96],[421,3],[422,3],[425,97],[423,3],[424,3],[558,3],[559,98],[557,99],[561,100],[675,101],[676,102],[696,103],[697,104],[698,3],[699,105],[700,106],[709,107],[702,108],[706,109],[714,110],[712,5],[713,111],[703,112],[715,3],[717,113],[718,114],[719,115],[708,116],[704,117],[728,118],[716,119],[743,120],[701,121],[744,122],[741,123],[742,5],[766,124],[691,125],[687,126],[689,127],[740,128],[682,129],[730,130],[729,3],[690,131],[737,132],[694,133],[738,3],[739,134],[692,135],[693,136],[688,137],[686,138],[681,3],[734,139],[747,140],[745,5],[677,5],[733,141],[678,12],[679,104],[680,142],[684,143],[683,144],[746,145],[685,146],[722,147],[720,113],[721,148],[731,12],[732,149],[735,150],[750,151],[751,152],[748,153],[749,154],[752,155],[753,156],[755,157],[727,158],[724,159],[725,4],[726,148],[757,160],[756,161],[763,162],[695,5],[759,163],[758,5],[761,164],[760,3],[762,165],[707,166],[736,167],[765,168],[764,5],[609,3],[562,3],[563,3],[566,169],[590,170],[567,3],[568,3],[569,5],[572,3],[570,3],[591,3],[573,3],[574,171],[575,3],[571,3],[576,5],[577,3],[578,172],[580,173],[581,3],[583,174],[584,173],[586,175],[592,176],[587,172],[588,172],[585,3],[593,177],[598,178],[610,179],[589,3],[579,172],[597,180],[564,3],[582,181],[595,182],[596,3],[594,3],[599,183],[600,5],[605,184],[601,5],[602,5],[603,5],[604,5],[565,3],[607,175],[606,3],[608,185],[626,186],[612,3],[616,187],[613,188],[617,3],[618,189],[490,190],[491,190],[492,191],[429,192],[493,193],[494,194],[495,195],[427,3],[496,196],[497,197],[498,198],[499,199],[500,200],[501,201],[502,201],[503,202],[504,203],[505,204],[506,205],[430,3],[428,3],[507,206],[508,207],[509,208],[551,209],[510,210],[511,211],[512,210],[513,212],[514,213],[515,214],[516,215],[517,215],[518,215],[519,216],[520,217],[521,218],[522,219],[523,220],[524,221],[525,221],[526,222],[527,3],[528,3],[529,223],[530,224],[531,225],[532,223],[533,226],[534,227],[535,228],[536,229],[537,230],[538,231],[539,232],[540,233],[541,234],[542,235],[543,236],[544,237],[545,238],[546,239],[547,240],[431,210],[432,3],[433,241],[434,242],[435,3],[436,243],[437,3],[481,244],[482,245],[483,246],[484,246],[485,247],[486,3],[487,193],[488,248],[489,245],[548,249],[549,250],[550,251],[625,252],[624,253],[552,254],[651,3],[650,255],[649,3],[658,3],[648,256],[659,257],[657,258],[646,3],[653,259],[654,256],[655,3],[647,260],[656,261],[660,262],[652,263],[623,264],[620,3],[622,265],[621,3],[619,3],[66,3],[261,266],[234,3],[212,267],[210,267],[260,268],[225,269],[224,269],[125,270],[76,271],[232,270],[233,270],[235,272],[236,270],[237,273],[136,274],[238,270],[209,270],[239,270],[240,275],[241,270],[242,269],[243,276],[244,270],[245,270],[246,270],[247,270],[248,269],[249,270],[250,270],[251,270],[252,270],[253,277],[254,270],[255,270],[256,270],[257,270],[258,270],[75,268],[78,273],[79,273],[80,273],[81,273],[82,273],[83,273],[84,273],[85,270],[87,278],[88,273],[86,273],[89,273],[90,273],[91,273],[92,273],[93,273],[94,273],[95,270],[96,273],[97,273],[98,273],[99,273],[100,273],[101,270],[102,273],[103,273],[104,273],[105,273],[106,273],[107,273],[108,270],[110,279],[109,273],[111,273],[112,273],[113,273],[114,273],[115,277],[116,270],[117,270],[131,280],[119,281],[120,273],[121,273],[122,270],[123,273],[124,273],[126,282],[127,273],[128,273],[129,273],[130,273],[132,273],[133,273],[134,273],[135,273],[137,283],[138,273],[139,273],[140,273],[141,270],[142,273],[143,284],[144,284],[145,284],[146,270],[147,273],[148,273],[149,273],[154,273],[150,273],[151,270],[152,273],[153,270],[155,273],[156,273],[157,273],[158,273],[159,273],[160,273],[161,270],[162,273],[163,273],[164,273],[165,273],[166,273],[167,273],[168,273],[169,273],[170,273],[171,273],[172,273],[173,273],[174,273],[175,273],[176,273],[177,273],[178,285],[179,273],[180,273],[181,273],[182,273],[183,273],[184,273],[185,270],[186,270],[187,270],[188,270],[189,270],[190,273],[191,273],[192,273],[193,273],[211,286],[259,270],[196,287],[195,288],[219,289],[218,290],[214,291],[213,290],[215,292],[204,293],[202,294],[217,295],[216,292],[203,3],[205,296],[118,297],[74,298],[73,273],[208,3],[200,299],[201,300],[198,3],[199,301],[197,273],[206,302],[77,303],[226,3],[227,3],[220,3],[223,269],[222,3],[228,3],[229,3],[221,304],[230,3],[231,3],[194,305],[207,306],[63,3],[64,3],[12,3],[10,3],[11,3],[16,3],[15,3],[2,3],[17,3],[18,3],[19,3],[20,3],[21,3],[22,3],[23,3],[24,3],[3,3],[25,3],[26,3],[4,3],[27,3],[31,3],[28,3],[29,3],[30,3],[32,3],[33,3],[34,3],[5,3],[35,3],[36,3],[37,3],[38,3],[6,3],[42,3],[39,3],[40,3],[41,3],[43,3],[7,3],[44,3],[49,3],[50,3],[45,3],[46,3],[47,3],[48,3],[8,3],[54,3],[51,3],[52,3],[53,3],[55,3],[9,3],[56,3],[65,3],[57,3],[58,3],[60,3],[59,3],[61,3],[1,3],[62,3],[14,3],[13,3],[456,307],[469,308],[453,309],[470,310],[479,311],[444,312],[445,313],[443,314],[478,254],[473,315],[477,316],[447,317],[466,318],[446,319],[476,320],[441,321],[442,315],[448,322],[449,3],[455,323],[452,322],[439,324],[480,325],[471,326],[459,327],[458,322],[460,328],[463,329],[457,330],[461,331],[474,254],[450,332],[451,333],[464,334],[440,310],[468,335],[467,322],[454,333],[462,336],[465,337],[472,3],[438,3],[475,338],[674,339],[629,340],[630,341],[628,342],[635,343],[636,344],[634,345],[638,346],[639,347],[637,342],[641,348],[642,349],[640,342],[611,5],[644,350],[645,351],[643,342],[663,352],[664,353],[662,354],[661,355],[665,356],[767,357],[666,358],[627,359],[668,360],[669,361],[670,362],[667,363],[672,364],[673,365],[671,358],[632,366],[633,367],[631,368]],"version":"6.0.3"} \ No newline at end of file diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..85c4a0e --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "rootDir": "./src", + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "ignoreDeprecations": "6.0", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/docs/contracts/examples/receipt-event.json b/docs/contracts/examples/receipt-event.json new file mode 100644 index 0000000..a1cc00f --- /dev/null +++ b/docs/contracts/examples/receipt-event.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "v1", + "messageType": "ReceiptEvent", + "traceId": "trace-20260701-000001", + "messageId": "msg-20260701-000001", + "channelId": "sms-channel-cmpp-001", + "createdAt": "2026-07-01T09:00:02.000Z", + "sequenceId": 2048, + "gatewayMessageId": "gw-msg-20260701-000001", + "receiptStatus": "delivered", + "rawStatus": "DELIVRD", + "deliveredAt": "2026-07-01T09:00:01.900Z" +} diff --git a/docs/contracts/examples/submit-command.json b/docs/contracts/examples/submit-command.json new file mode 100644 index 0000000..4178c05 --- /dev/null +++ b/docs/contracts/examples/submit-command.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": "v1", + "messageType": "SubmitCommand", + "traceId": "trace-20260701-000001", + "messageId": "msg-20260701-000001", + "channelId": "sms-channel-cmpp-001", + "createdAt": "2026-07-01T09:00:00.000Z", + "tenantId": "tenant-demo", + "applicationId": "app-demo", + "taskId": "task-20260701-000001", + "submitId": "submit-20260701-000001", + "phoneNumber": "13800138000", + "content": "您的验证码为 123456,5 分钟内有效。", + "signature": "测试平台", + "templateId": "tpl-demo-code", + "billingUnits": 1, + "route": { + "channelCode": "CMCC-CMPP-DEMO", + "cmppAccountCode": "cmpp-account-demo", + "priority": 10, + "rateLimitPerSecond": 500 + }, + "cmpp": { + "serviceId": "CMPP", + "srcId": "106900000000", + "registeredDelivery": 1, + "msgFmt": 15, + "feeUserType": 2, + "feeCode": "0", + "feeType": "01" + }, + "retry": { + "attempt": 0, + "maxAttempts": 3 + } +} diff --git a/docs/contracts/examples/submit-result.json b/docs/contracts/examples/submit-result.json new file mode 100644 index 0000000..0ecc438 --- /dev/null +++ b/docs/contracts/examples/submit-result.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": "v1", + "messageType": "SubmitResult", + "traceId": "trace-20260701-000001", + "messageId": "msg-20260701-000001", + "channelId": "sms-channel-cmpp-001", + "createdAt": "2026-07-01T09:00:00.120Z", + "sequenceId": 1024, + "gatewayMessageId": "gw-msg-20260701-000001", + "submitStatus": "accepted", + "submittedAt": "2026-07-01T09:00:00.118Z" +} diff --git a/docs/contracts/examples/uplink-event.json b/docs/contracts/examples/uplink-event.json new file mode 100644 index 0000000..41c4cfb --- /dev/null +++ b/docs/contracts/examples/uplink-event.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "v1", + "messageType": "UplinkEvent", + "traceId": "trace-20260701-uplink-000001", + "messageId": "uplink-20260701-000001", + "channelId": "sms-channel-cmpp-001", + "createdAt": "2026-07-01T09:01:00.000Z", + "sequenceId": 4096, + "phoneNumber": "13800138000", + "destId": "106900000000", + "content": "TD", + "receivedAt": "2026-07-01T09:00:59.800Z" +} diff --git a/docs/contracts/gateway-queue-messages.schema.json b/docs/contracts/gateway-queue-messages.schema.json new file mode 100644 index 0000000..b6eb029 --- /dev/null +++ b/docs/contracts/gateway-queue-messages.schema.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cmpp-platform.local/schemas/gateway-queue-messages.schema.json", + "title": "CMPP Gateway Queue Messages", + "oneOf": [ + { "$ref": "#/$defs/SubmitCommand" }, + { "$ref": "#/$defs/SubmitResult" }, + { "$ref": "#/$defs/ReceiptEvent" }, + { "$ref": "#/$defs/UplinkEvent" } + ], + "$defs": { + "Envelope": { + "type": "object", + "required": ["schemaVersion", "messageType", "traceId", "messageId", "channelId", "createdAt"], + "properties": { + "schemaVersion": { "const": "v1" }, + "messageType": { + "enum": ["SubmitCommand", "SubmitResult", "ReceiptEvent", "UplinkEvent"] + }, + "traceId": { "type": "string", "minLength": 8 }, + "messageId": { "type": "string", "minLength": 8 }, + "channelId": { "type": "string", "minLength": 1 }, + "createdAt": { "type": "string", "format": "date-time" } + } + }, + "SubmitCommand": { + "allOf": [ + { "$ref": "#/$defs/Envelope" }, + { + "type": "object", + "required": [ + "messageType", + "tenantId", + "applicationId", + "submitId", + "phoneNumber", + "content", + "signature", + "templateId", + "billingUnits", + "route", + "cmpp", + "retry" + ], + "properties": { + "messageType": { "const": "SubmitCommand" }, + "tenantId": { "type": "string", "minLength": 1 }, + "applicationId": { "type": "string", "minLength": 1 }, + "taskId": { "type": "string" }, + "submitId": { "type": "string", "minLength": 1 }, + "phoneNumber": { "type": "string", "pattern": "^1[3-9][0-9]{9}$" }, + "content": { "type": "string", "minLength": 1 }, + "signature": { "type": "string", "minLength": 1 }, + "templateId": { "type": "string", "minLength": 1 }, + "billingUnits": { "type": "integer", "minimum": 1 }, + "route": { + "type": "object", + "required": ["channelCode", "cmppAccountCode", "priority"], + "properties": { + "channelCode": { "type": "string", "minLength": 1 }, + "cmppAccountCode": { "type": "string", "minLength": 1 }, + "priority": { "type": "integer", "minimum": 0 }, + "rateLimitPerSecond": { "type": "integer", "minimum": 1 } + } + }, + "cmpp": { + "type": "object", + "required": ["serviceId", "srcId", "registeredDelivery", "msgFmt"], + "properties": { + "serviceId": { "type": "string", "minLength": 1 }, + "srcId": { "type": "string", "minLength": 1 }, + "registeredDelivery": { "type": "integer", "enum": [0, 1] }, + "msgFmt": { "type": "integer", "enum": [8, 15] }, + "feeUserType": { "type": "integer", "minimum": 0 }, + "feeCode": { "type": "string" }, + "feeType": { "type": "string" } + } + }, + "retry": { + "type": "object", + "required": ["attempt", "maxAttempts"], + "properties": { + "attempt": { "type": "integer", "minimum": 0 }, + "maxAttempts": { "type": "integer", "minimum": 1 } + } + } + } + } + ] + }, + "SubmitResult": { + "allOf": [ + { "$ref": "#/$defs/Envelope" }, + { + "type": "object", + "required": ["messageType", "sequenceId", "gatewayMessageId", "submitStatus", "submittedAt"], + "properties": { + "messageType": { "const": "SubmitResult" }, + "sequenceId": { "type": "integer", "minimum": 0 }, + "gatewayMessageId": { "type": "string", "minLength": 1 }, + "submitStatus": { "enum": ["accepted", "rejected", "timeout"] }, + "errorCode": { "type": "string" }, + "errorMessage": { "type": "string" }, + "submittedAt": { "type": "string", "format": "date-time" } + } + } + ] + }, + "ReceiptEvent": { + "allOf": [ + { "$ref": "#/$defs/Envelope" }, + { + "type": "object", + "required": ["messageType", "sequenceId", "gatewayMessageId", "receiptStatus", "rawStatus", "deliveredAt"], + "properties": { + "messageType": { "const": "ReceiptEvent" }, + "sequenceId": { "type": "integer", "minimum": 0 }, + "gatewayMessageId": { "type": "string", "minLength": 1 }, + "receiptStatus": { "enum": ["delivered", "undelivered", "unknown"] }, + "rawStatus": { "type": "string", "minLength": 1 }, + "errorCode": { "type": "string" }, + "deliveredAt": { "type": "string", "format": "date-time" } + } + } + ] + }, + "UplinkEvent": { + "allOf": [ + { "$ref": "#/$defs/Envelope" }, + { + "type": "object", + "required": ["messageType", "sequenceId", "phoneNumber", "destId", "content", "receivedAt"], + "properties": { + "messageType": { "const": "UplinkEvent" }, + "sequenceId": { "type": "integer", "minimum": 0 }, + "phoneNumber": { "type": "string", "pattern": "^1[3-9][0-9]{9}$" }, + "destId": { "type": "string", "minLength": 1 }, + "content": { "type": "string", "minLength": 1 }, + "receivedAt": { "type": "string", "format": "date-time" } + } + } + ] + } + } +} diff --git a/docs/first-version-development-requirements.md b/docs/first-version-development-requirements.md new file mode 100644 index 0000000..6341939 --- /dev/null +++ b/docs/first-version-development-requirements.md @@ -0,0 +1,1094 @@ +# CMPP 短信平台第一版开发需求文档 + +## 0. 文档前提 + +本文基于当前前端设计原型整理,用于交给 Codex 或开发团队执行第一版落地开发。 + +当前确认:第一版保留短信业务,排除彩信功能;账户计费、充值套餐、账单流水进入第一版开发范围。彩信服务、彩信应用/签名/模板 Tab,以及运营端彩信相关菜单标记为“待开发”;业务性能指标为“平台可稳定入队并调度 500 条短信/秒,实际向通道 submit 受通道限速配置控制”。 + +## 1. 项目目标 + +建设一个短信平台第一版,支持企业客户在客户端完成短信应用、模板、签名、号码导入、短信发送、批量任务查询、发送明细查询、上行短信查询;支持运营端完成企业管理、企业应用/签名/模板管理、审核、通道配置、通道签名报备、报备任务导出/回执导入、发送监控、任务进度、短信记录、上行记录、安全控制和系统管理。 + +第一版必须满足: + +- 支持最高 500 条短信/秒的提交吞吐。 +- 支持批量任务异步发送,前端提交后可查询任务进度。 +- 支持通道级限速、失败重试、回执同步和发送记录追踪。 +- 支持企业、应用、签名、模板、通道、通道组、报备任务等核心配置数据的后台维护。 +- 彩信功能仅保留菜单占位或隐藏,不进入第一版开发范围。 +- 账户计费、充值套餐、账单流水进入第一版范围,并与发送记录形成可对账闭环。 + +## 2. 角色与权限 + +### 2.1 客户端企业管理员 + +可管理本企业短信应用、短信模板、短信签名、发送任务、上行短信、企业认证、企业用户和系统日志。 + +### 2.2 客户端普通用户 + +可按授权查看应用、模板、签名、发送任务和发送明细;是否允许创建和发送需由企业管理员配置。 + +### 2.3 运营端管理员 + +可管理全平台企业、应用、签名、模板、审核、通道、通道组、报备任务、发送监控、短信记录、上行记录、黑名单、敏感词、用户、手机号段库和引流字段库。 + +### 2.4 运营审核员 + +可处理企业认证审核、短信审核、短信模板审核、签名/模板/发送内容相关审核。 + +## 3. 第一版范围 + +### 3.1 客户端范围 + +保留并开发: + +- 工作台 +- 短信发送 +- 查看批量任务 +- 短信发送详情 +- 查看上行短信 +- 短信应用 +- 模板管理 +- 签名与引流信息 +- 充值套餐 +- 账单流水 +- 企业认证 +- 用户管理 +- 账号设置 +- 系统日志 + +暂不开发: + +- 彩信服务分组下全部菜单 + +### 3.2 运营端范围 + +保留并开发: + +- 运营看板、发送监控、数据统计、客户管理 +- 企业管理、企业应用管理、企业签名管理、企业模板管理 +- 企业认证审核、短信审核、短信模板审核 +- 短信通道管理、短信通道组管理 +- 报备任务、报备记录 +- 短信任务进度、短信记录、短信上行记录 +- 充值记录、账单流水、套餐配置、计费规则 +- 企业黑名单、全局黑名单、敏感词管理 +- 用户管理、手机号段库、引流信息字段库 + +暂不开发: + +- 彩信模板审核 +- 彩信通道管理 +- 彩信任务进度 +- 彩信记录 +- 企业应用管理的彩信应用 Tab +- 企业签名管理的彩信签名 Tab +- 企业模板管理的彩信模板 Tab + +## 4. 核心业务流程 + +### 4.1 企业入驻与认证 + +1. 客户端企业管理员提交企业认证资料。 +2. 运营端在企业认证审核中查看资料并审核通过或驳回。 +3. 审核通过后企业可创建短信应用、签名、模板并发起短信发送。 +4. 驳回时必须记录驳回原因,客户端可查看并重新提交。 + +### 4.2 短信应用管理 + +1. 客户端创建短信应用,填写应用名称、场景、回调地址、限额等信息。 +2. 运营端可查看企业应用,并支持按企业名称、应用名称、状态搜索。 +3. 运营端可启用、停用、编辑应用。 +4. 应用必须归属企业,并关联后续签名、模板和发送任务。 + +### 4.3 签名与引流信息 + +1. 客户端创建短信签名,提交签名名称、用途、证明材料、引流信息。 +2. 运营端企业签名管理查看签名资料。 +3. 签名需完成企业内部审核和通道报备,状态包括草稿、待审核、已通过、已驳回、报备中、报备通过、报备失败。 +4. 已通过且报备通过的签名才允许发送。 + +### 4.4 模板管理与审核 + +1. 客户端创建短信模板,填写模板名称、短信内容、变量、应用、签名。 +2. 系统校验敏感词、字数、变量格式和签名匹配。 +3. 运营端短信模板审核可通过或驳回模板。 +4. 审核通过的模板才允许在短信发送中选择。 + +### 4.5 短信发送 + +1. 客户端选择短信应用、签名、模板。 +2. 输入或导入接收手机号,支持手动输入和文件导入。 +3. 系统校验号码格式、黑名单、手机号段、企业余额/额度、模板变量。 +4. 客户端选择立即发送或定时发送。 +5. 提交后生成批量任务,任务进入待审核或待发送状态。 +6. 系统执行风控规则,命中拒绝规则则直接拒绝并记录原因,命中人工审核规则则进入运营端短信审核。 +7. 若命中审核策略,运营端短信审核通过后进入发送队列,审核页面必须展示进入审核的原因。 +8. 发送服务按通道组路由、通道限速、企业限速执行提交。 +9. 平台批量任务只记录客户端创建的发送任务;API 调用和 CMPP 对接发送不进入批量任务。 +10. 所有来源的短信,包括平台批量任务、API 调用、CMPP 对接发送,全部按手机号维度进入短信记录。 +11. 任务进度、发送详情和短信记录实时或准实时更新。 + +### 4.6 通道配置与路由 + +1. 运营端配置短信通道,包括通道名称、运营商、单价、网关地址、端口、企业代码、账号、密码、接入号、协议参数、启停状态。 +2. 运营端配置短信通道组,定义通道优先级、运营商适配、区域适配、权重和故障切换。 +3. 发送时根据企业、应用、签名、模板、号码归属、通道状态和通道组策略选择通道。 +4. 通道异常时应支持熔断、降级、切换备用通道和失败重试。 + +### 4.7 通道签名报备 + +1. 运营端在通道配置中维护签名报备字段。 +2. 客户端上传签名资料。 +3. 运营端审核企业签名资料。 +4. 运营端在通道资料更新后生成通道签名报备任务。 +5. 运营端在报备任务中导出通道报备资料。 +6. 运营端在报备任务或通道报备详情页导入通道回执。 +7. 系统根据回执同步签名在各通道的报备状态。 +8. 报备记录保留每次导出、导入、状态变更和操作人。 + +### 4.8 回执与上行 + +1. 通道回执接入后更新发送记录状态。 +2. 回执状态至少包括提交成功、提交失败、发送成功、发送失败、未知、超时。 +3. 提交后 72 小时仍为未知的短信转超时,返回客户失败。 +4. 超时前允许人工同步、重新拉取回执或接收供应商二次回执;所有二次回执必须记录历史。 +5. 上行短信接入后优先按手机号、接入号、企业/应用、时间窗口匹配下发记录。 +6. 接入号匹配不到企业/应用时,展示近期平台对此手机号下发的短信供人工判断。 +7. 完全匹配不到的上行短信仍需入库,并展示为未匹配。 +8. 客户端可查看本企业上行短信,运营端可查看全平台上行短信。 + +### 4.9 账户计费 + +1. 客户端可查看充值套餐、购买或申请充值套餐、查看账单流水。 +2. 发送创建时按短信内容计费条数、企业单价或套餐规则生成预估费用,计费条数只按 70/67 字规则拆分。 +3. 平台需在发送前检查企业账户余额、套餐余量或授信额度。 +4. 发送链路需记录计费条数、计费单价、计费金额、账务状态。 +5. 账单流水与短信记录可追溯关联,支持按企业、应用、任务、手机号、时间对账。 +6. 最终失败、超时失败需要退费。 +7. 计费口径可配置为按提交成功计费或按回执成功计费。 + +## 5. 功能需求 + +### 5.1 客户端工作台 + +- 展示短信余量或可发送额度、今日发送量、成功率、待审核事项、快捷入口。 +- 展示最近发送批次、发送趋势、签名/模板状态。 +- 数据范围限定为当前企业。 + +### 5.2 客户端短信发送 + +- 支持选择短信应用、签名、模板。 +- 支持模板变量填充。 +- 支持手机号手动输入、粘贴、文件导入。 +- 导入号码文件格式支持 CSV、TXT,最大文件大小 20 MB。 +- 单任务最大号码数默认 100 万条。 +- 支持去重、非法号码提示、黑名单拦截提示。 +- 支持立即发送和定时发送。 +- 不允许修改已审核模板主体,只允许填写模板变量;如需修改主体,必须新建模板重新审核。 +- 不符合模板的短信按应用配置处理:拒绝发送、进入人工审核或直接发送。 +- 敏感词命中第一期直接拒绝发送,并记录拒绝原因。 +- 提交后展示任务编号和任务状态。 + +### 5.3 客户端批量任务 + +- 支持按任务编号、应用、发送时间、状态搜索。 +- 展示任务总量、成功数、失败数、未知数、发送进度、创建人。 +- 支持查看任务详情和导出明细;默认企业管理员可导出,普通用户需授权。 + +### 5.4 客户端发送详情 + +- 支持按手机号、内容、回执、时间搜索。 +- 展示手机号、短信内容、计费条数、通道、提交时间、回执时间、状态。 +- 支持查看单条发送链路。 + +### 5.5 客户端上行短信 + +- 支持按手机号、内容、接收时间搜索。 +- 展示上行内容、接入号、匹配下发记录、接收时间。 + +### 5.6 客户端短信应用 + +- 支持新增、编辑、启用、停用应用。 +- 支持配置回调地址、IP 白名单、应用密钥、日发送限额。 +- 支持配置“不符合模板的短信”处理策略:拒绝发送、人工审核、直接发送。 +- 支持配置应用级风控阈值:单任务最大号码数、重复号码比例、非法号码比例、黑名单命中比例、非工作时间大批量发送策略、短时间任务创建频控。 +- 默认阈值:重复号码比例 30%、非法号码比例 10%、黑名单命中比例 5%、同一企业/应用 10 分钟内最多创建 10 次任务。 +- 应用密钥需支持重置,并记录操作日志。 + +### 5.7 客户端模板管理 + +- 支持新增、编辑、提交审核、撤回、删除草稿。 +- 支持变量识别和字数/计费条数预估。 +- 支持查看审核记录和驳回原因。 + +### 5.8 客户端签名管理 + +- 支持新增、编辑、提交审核、上传证明材料。 +- 支持维护引流信息。 +- 支持查看通道报备状态。 + +### 5.9 客户端账户计费 + +- 充值套餐:展示可购买套餐、套餐价格、短信条数、有效期、适用范围。 +- 账单流水:展示充值、冻结、扣费、退费、调整等流水。 +- 账单流水需关联短信任务、短信记录或人工调整单据。 +- 客户端账号设置属于系统管理,不允许客户自行配置通道或通道组。 + +### 5.10 运营端看板与监控 + +- 展示总发送量、成功率、通道健康度、待处理审核数。 +- 发送监控展示通道状态、发送趋势、失败率、积压队列。 +- 数据统计支持按企业、应用、通道、日期统计。 + +### 5.11 运营端客户与企业 + +- 支持企业列表、企业详情、新增、编辑、启用、停用。 +- 支持企业认证资料审核。 +- 支持查看企业下应用、签名、模板、发送记录、报备记录。 + +### 5.12 运营端审核 + +- 企业认证审核:通过、驳回、查看材料。 +- 短信模板审核:通过、驳回、敏感词提示、变量检查。 +- 短信审核:查看短信内容、号码量、计费条数、进入审核原因、命中规则、风险原因;支持通过、驳回。 +- 审核动作集中在审核中心;企业应用/签名/模板管理页面只做查询、维护、停用、备注和查看审核记录。 + +### 5.13 运营端通道管理 + +- 支持新增、编辑、删除、启用、停用短信通道。 +- 支持发送测试短信。 +- 支持查看通道成功率、未知率、失败率、累计发送量。 +- 支持进入通道报备详情。 + +### 5.14 运营端通道组管理 + +- 支持新增、编辑通道组。 +- 支持配置运营商、区域、优先级、权重、备用通道、限速。 +- 支持设置企业或应用绑定关系。 + +### 5.15 运营端报备任务 + +- 支持按企业、签名、通道、任务状态、时间搜索。 +- 支持生成报备任务。 +- 支持在报备任务中导出通道报备资料。 +- 支持在报备任务中导入回执。 +- 支持查看报备任务详情和处理历史。 + +### 5.16 运营端报备记录 + +- 记录报备任务生成、导出、导入、状态同步、失败原因。 +- 支持按企业、签名、通道、状态、操作时间搜索。 + +### 5.17 安全控制 + +- 企业黑名单:企业维度号码拦截。 +- 全局黑名单:平台维度号码拦截。 +- 敏感词管理:发送前和审核时命中提示或拦截。 +- 手机号段库:用于运营商识别和路由。 +- 引流信息字段库:用于签名/报备资料结构化采集。 + +### 5.18 风控规则闭环 + +- 运营端提供风控规则配置页面,可按平台、企业、应用维度配置规则。 +- 第一版支持规则:单任务最大号码数、重复号码比例、非法号码比例、黑名单命中比例、非工作时间大批量营销发送、短时间任务创建频控、模板变量异常。 +- 默认阈值:重复号码比例 30%、非法号码比例 10%、黑名单命中比例 5%、同一企业/应用 10 分钟内最多创建 10 次任务。 +- 每条规则需配置启停、阈值、处理动作、适用范围、优先级。 +- 处理动作包括:拒绝发送、进入人工审核、仅记录预警。 +- 命中规则后必须生成风控命中记录,包含规则编号、规则名称、阈值、实际值、处理动作、命中时间。 +- 进入短信审核的任务必须展示进入审核原因和命中的风控规则。 +- 直接拒绝的任务必须向客户端返回可读原因,并写入系统日志。 + +### 5.19 运营端账户计费 + +- 套餐配置:支持配置套餐名称、价格、短信条数、有效期、适用企业范围、启停状态。 +- 充值记录:支持人工充值、套餐购买记录、授信额度调整。 +- 账单流水:支持冻结、扣费、退费、解冻、人工调整、失败返还。 +- 计费规则:支持按短信计费条数、企业单价、套餐余量、授信额度计算费用。 +- 账务流水必须与短信记录形成可追溯关系,支持对账导出。 +- 最终失败、超时失败需要退费。 +- 计费口径可配置为按提交成功计费或按回执成功计费。 + +### 5.20 数据保存与清理 + +- 发送记录保存 12 个月。 +- 支持运营端手动按月清除发送记录。 +- 冷热分层由技术方案自行设计,但不得影响最近 12 个月在线查询与对账。 + +### 5.21 系统管理与审计 + +- 用户管理支持角色、权限、启停、重置密码。 +- 系统日志记录登录、配置变更、审核、发送、导入导出、密钥重置等操作。 + +## 6. 非功能需求 + +### 6.1 性能 + +- 平台峰值提交能力:500 条短信/秒。 +- 批量任务提交接口响应:创建任务后 2 秒内返回任务编号,不同步完成发送。 +- 查询接口 P95 响应时间:列表查询小于 500 ms,详情查询小于 800 ms。 +- 发送进度延迟:任务进度统计延迟不超过 5 秒。 +- 回执处理:单通道回执接入后 10 秒内完成状态更新,异常积压需告警。 + +### 6.2 可用性 + +- 发送链路服务可横向扩容。 +- 通道异常不影响其他通道发送。 +- 队列消息必须可重试、可追踪、可补偿。 +- 关键配置变更必须记录审计日志。 + +### 6.3 数据安全 + +- 企业数据必须按租户隔离。 +- 手机号、通道密码、应用密钥等敏感字段加密或脱敏展示。 +- 导入文件需做格式校验、大小限制、病毒/恶意内容防护。 +- 导出文件需鉴权并记录日志。 + +### 6.4 合规与风控 + +- 支持敏感词拦截。 +- 支持黑名单拦截。 +- 支持按企业、应用、通道设置日限额、秒级限速。 +- 营销类短信不单独硬编码强制审核,由应用配置和风控规则共同决定处理动作。 + +## 7. 推荐技术栈 + +### 7.1 前端 + +- React 19 +- TypeScript +- Vite +- React Router +- Zustand 或 TanStack Query +- ECharts +- Lucide React +- CSS Modules 或现有全局 CSS 体系 + +说明:现有原型已使用 React、TypeScript、Vite、React Router、Zustand、ECharts、Lucide,可延续。 + +### 7.2 后端 + +采用 TypeScript 业务后台 + Go CMPP 网关: + +- 后端 API:NestJS + TypeScript +- DB:PostgreSQL +- ORM:Prisma +- 队列:Redis + BullMQ +- 缓存/限速:Redis +- 文件:MinIO +- API 文档:OpenAPI/Swagger +- 监控:Prometheus + Grafana +- 日志:Loki 或 ELK +- 网关:Go 实现真实 CMPP Gateway,第一版必须接入真实 CMPP 长连接能力 + +### 7.3 短信网关 + +- Go CMPP Gateway 作为独立服务部署,不和 NestJS 业务后台耦合。 +- 第一版支持真实 CMPP 2.0/3.0 长连接能力,后续可扩展 SGIP、SMGP、HTTP 通道。 +- 网关负责通道连接、登录认证、长连接保活、submit、deliver、active test、重连、滑动窗口、sequence 管理、回执与上行接收。 +- NestJS 业务后台负责企业、应用、模板、签名、报备、审核、计费、风控、路由决策和发送任务编排。 +- NestJS 与 Go Gateway 之间通过 Redis/BullMQ 队列或内部 gRPC/HTTP 通信;第一版建议使用队列提交发送指令、队列回传提交结果和回执事件。 +- Go Gateway 不直接承担业务审核、账务扣费、签名报备判断,只执行已被业务后台路由后的通道提交。 + +### 7.4 Go CMPP 开源项目评估结论 + +可参考开源项目,但不建议直接把完整开源网关作为核心生产服务不加改造使用。建议策略: + +- 协议层优先评估并复用成熟 Go CMPP 协议库,降低从零实现 CMPP PDU、编解码、sequence、滑动窗口、deliver 解析的风险。 +- 网关服务层由本项目自研,围绕本系统的通道配置、限速、发送队列、回执幂等、监控、日志、故障切换、账务关联来设计。 +- 对开源网关项目可作为参考实现或测试工具,不直接绑定业务模型。 + +初步候选: + +- `bigwhite/gocmpp`:Go CMPP 协议库,适合作为协议层参考或依赖候选。 +- `JoeCao/cmpp-gateway`:基于 gocmpp 的网关项目,包含 gateway、backend、simulator 等模块,适合作为架构参考和联调模拟参考。 + +落地前需要完成一次技术 Spike: + +1. 拉取候选项目,确认 License、维护活跃度、CMPP 2.0/3.0 支持情况。 +2. 用模拟 SMSC 完成 connect、submit、deliver、active test、disconnect 流程。 +3. 压测单连接和多连接 submit 能力,验证 500 条/秒调度下的网关稳定性。 +4. 检查异常场景:断线重连、sequence 回绕、窗口满、回执重复、SMSC 慢响应。 +5. 决定最终依赖方式:直接依赖协议库、fork 改造、或完全自研协议层。 + +## 8. 建议系统架构 + +### 8.1 服务拆分 + +第一版可采用模块化单体加独立发送 Worker,避免过早微服务化: + +- Web/API 服务:认证、企业、应用、签名、模板、审核、通道、报备、查询。 +- Send Worker:消费发送队列,执行路由、限速、提交通道。 +- Receipt Worker:处理通道回执、更新发送状态。 +- Report Worker:处理报备资料导出、回执导入、状态同步。 +- Gateway Adapter:CMPP 通道连接与协议适配。 + +后续可拆为独立服务: + +- tenant-service +- config-service +- audit-service +- send-service +- gateway-service +- report-service +- statistics-service + +### 8.2 发送链路 + +1. API 创建批量任务。 +2. 拆分号码和变量,生成 message_record。 +3. 写入数据库并投递 send.queue。 +4. Send Worker 消费消息,校验黑名单、敏感词、限额。 +5. 路由选择通道。 +6. 通过通道限速器控制 TPS。 +7. 调用 Gateway Adapter 提交短信。 +8. 写入 submit 状态。 +9. Receipt Worker 接收回执并更新最终状态。 +10. 统计任务进度。 + +### 8.3 500 条/秒实现建议 + +- 创建任务与发送解耦,提交接口只负责入库和投递队列。 +- Send Worker 多实例部署,每实例处理固定通道分片或队列分片。 +- 使用 Redis Token Bucket 或本地令牌桶加 Redis 协调实现通道限速。 +- 单条发送记录以 message_id 全局唯一,所有提交和回执处理幂等。 +- 批量插入 message_record,避免逐条事务。 +- 按任务、日期或企业对发送明细表做分区。 +- 热查询统计写入 Redis 或统计表,避免实时 count 大表。 + +## 9. 核心数据模型 + +### 9.1 租户与用户 + +- tenant:企业主体。 +- tenant_certification:企业认证资料。 +- user:用户。 +- role、permission、user_role:权限。 +- operation_log:操作日志。 + +### 9.2 短信配置 + +- sms_application:短信应用。 +- sms_signature:短信签名。 +- signature_material:签名证明材料。 +- sms_template:短信模板。 +- template_variable:模板变量。 +- audit_record:审核记录。 + +### 9.3 通道与路由 + +- sms_channel:短信通道。 +- sms_channel_group:通道组。 +- sms_channel_group_item:通道组通道明细。 +- channel_route_rule:路由规则。 +- channel_health_metric:通道健康指标。 + +### 9.4 报备 + +- channel_report_field:通道报备字段配置。 +- signature_report_material:签名报备资料。 +- channel_signature_report_task:通道签名报备任务。 +- channel_signature_report_record:报备记录。 +- report_export_file:导出文件。 +- report_receipt_import:回执导入批次。 + +### 9.5 发送 + +- sms_batch_task:批量任务。 +- sms_message_record:单号码发送记录,所有平台任务、API 调用、CMPP 对接发送均进入此表。 +- sms_api_request:API 调用批次记录。 +- cmpp_submit_session:CMPP 对接提交会话或批次记录。 +- sms_submit_record:通道提交记录。 +- sms_receipt_record:短信回执。 +- sms_uplink_message:上行短信。 +- sms_send_audit:短信发送审核。 + +### 9.6 风控与基础数据 + +- enterprise_blacklist:企业黑名单。 +- global_blacklist:全局黑名单。 +- sensitive_word:敏感词。 +- risk_rule:风控规则配置。 +- risk_rule_hit:风控规则命中记录。 +- phone_segment:手机号段库。 +- drainage_field:引流字段库。 + +### 9.7 账户计费 + +- billing_plan:充值套餐。 +- tenant_account:企业账户。 +- account_balance:余额或套餐余量。 +- account_transaction:账户流水。 +- billing_rule:计费规则。 +- sms_billing_record:短信计费记录。 +- recharge_order:充值订单或人工充值记录。 + +## 10. 接口范围 + +### 10.1 客户端 API + +- `POST /api/client/auth/login` +- `GET /api/client/dashboard` +- `GET /api/client/applications` +- `POST /api/client/applications` +- `PUT /api/client/applications/{id}` +- `POST /api/client/applications/{id}/secret/reset` +- `GET /api/client/signatures` +- `POST /api/client/signatures` +- `POST /api/client/signatures/{id}/submit` +- `GET /api/client/templates` +- `POST /api/client/templates` +- `POST /api/client/templates/{id}/submit` +- `POST /api/client/sms/tasks` +- `GET /api/client/sms/tasks` +- `GET /api/client/sms/tasks/{id}` +- `GET /api/client/sms/messages` +- `GET /api/client/sms/uplinks` +- `GET /api/client/billing/plans` +- `POST /api/client/billing/orders` +- `GET /api/client/billing/transactions` +- `POST /api/client/enterprise-certification` +- `GET /api/client/users` +- `GET /api/client/system-logs` + +### 10.2 运营端 API + +- `GET /api/admin/dashboard` +- `GET /api/admin/monitor` +- `GET /api/admin/statistics` +- `GET /api/admin/enterprises` +- `POST /api/admin/enterprises` +- `PUT /api/admin/enterprises/{id}` +- `GET /api/admin/enterprise-applications` +- `GET /api/admin/enterprise-signatures` +- `GET /api/admin/enterprise-templates` +- `POST /api/admin/audits/{id}/approve` +- `POST /api/admin/audits/{id}/reject` +- `GET /api/admin/channels` +- `POST /api/admin/channels` +- `PUT /api/admin/channels/{id}` +- `POST /api/admin/channels/{id}/test` +- `GET /api/admin/channels/{id}/reports` +- `POST /api/admin/channel-groups` +- `GET /api/admin/report-tasks` +- `POST /api/admin/report-tasks/generate` +- `POST /api/admin/report-tasks/{id}/export` +- `POST /api/admin/report-tasks/{id}/receipt-import` +- `GET /api/admin/report-records` +- `GET /api/admin/sms/tasks` +- `GET /api/admin/sms/records` +- `GET /api/admin/sms/uplinks` +- `GET /api/admin/blacklists/enterprise` +- `GET /api/admin/blacklists/global` +- `GET /api/admin/sensitive-words` +- `GET /api/admin/risk-rules` +- `POST /api/admin/risk-rules` +- `PUT /api/admin/risk-rules/{id}` +- `GET /api/admin/risk-rule-hits` +- `GET /api/admin/billing/plans` +- `POST /api/admin/billing/plans` +- `PUT /api/admin/billing/plans/{id}` +- `GET /api/admin/billing/transactions` +- `POST /api/admin/billing/recharges` +- `GET /api/admin/phone-segments` +- `GET /api/admin/drainage-fields` +- `GET /api/admin/users` + +### 10.3 通道回调 API + +- `POST /api/gateway/receipts/{channelCode}` +- `POST /api/gateway/uplinks/{channelCode}` + +如使用 CMPP 长连接,回执和上行由 Gateway Adapter 直接消费协议消息,不一定暴露 HTTP 回调。 + +## 11. 状态定义 + +### 11.1 审核状态 + +- draft:草稿 +- pending:待审核 +- approved:已通过 +- rejected:已驳回 + +### 11.2 报备状态 + +- not_required:无需报备 +- waiting_material:待资料 +- pending:待报备 +- exporting:导出中 +- submitted:已提交通道 +- approved:报备通过 +- rejected:报备失败 +- partial_approved:部分通过 + +### 11.3 发送任务状态 + +- draft:草稿 +- pending_audit:待审核 +- rejected:审核驳回 +- scheduled:待定时发送 +- queued:排队中 +- sending:发送中 +- completed:已完成 +- canceled:已取消 +- failed:失败 + +### 11.4 单条短信状态 + +- created:已创建 +- rejected:风控拒绝或规则拒绝 +- filtered:已拦截 +- queued:待发送 +- submitted:已提交 +- submit_failed:提交失败 +- delivered:发送成功 +- undelivered:发送失败 +- unknown:未知 +- timeout:超时 + +### 11.5 短信来源 + +- platform_task:客户端平台批量任务 +- api:企业 API 调用 +- cmpp: 企业 CMPP 对接发送 + +### 11.6 风控处理动作 + +- reject:拒绝发送 +- manual_audit:进入人工审核 +- warn_only:仅记录预警 + +### 11.7 账务状态 + +- estimated:已预估 +- frozen:已冻结 +- charged:已扣费 +- refunded:已退费 +- released:已解冻 +- adjusted:人工调整 + +## 12. 实施步骤 + +### 阶段 1:工程初始化 + +1. 确认第一版范围,关闭或隐藏彩信菜单,保留账户计费菜单。 +2. 建立前端、NestJS API、Go CMPP Gateway 的仓库结构或 monorepo 工作区。 +3. 配置代码规范、环境变量、构建、Docker Compose。 +4. 建立 Prisma schema 和数据库迁移流程。 +5. 建立 OpenAPI 文档和前端 API Client 生成流程。 +6. 建立 Go Gateway 的配置、日志、监控、健康检查和本地模拟 SMSC 联调环境。 + +### 阶段 2:基础能力 + +1. 实现登录、用户、角色、权限。 +2. 实现租户隔离。 +3. 实现文件上传和对象存储。 +4. 实现操作日志。 +5. 实现基础字典:手机号段、敏感词、黑名单、引流字段。 +6. 实现账户、套餐、账务流水基础模型。 + +### 阶段 3:企业与配置 + +1. 实现企业认证。 +2. 实现短信应用。 +3. 实现短信签名和材料。 +4. 实现短信模板和变量解析。 +5. 实现应用级不符合模板短信处理策略。 +6. 实现审核记录与审核工作台。 + +### 阶段 4:通道与报备 + +1. 实现短信通道 CRUD。 +2. 实现通道组与路由规则。 +3. 实现通道签名报备字段配置。 +4. 实现报备任务生成。 +5. 实现报备资料导出。 +6. 实现回执导入和报备状态同步。 + +### 阶段 5:发送链路 + +1. 实现批量任务创建。 +2. 实现号码导入、校验、去重、拆分。 +3. 实现风控规则配置、规则命中、审核原因展示。 +4. 实现发送审核策略。 +5. 实现发送前账户校验、预估、冻结或扣费。 +6. 实现发送队列。 +7. 实现 Send Worker。 +8. 实现 Go CMPP Gateway,支持真实 CMPP 长连接、submit、deliver、active test、重连。 +9. 实现回执处理、72 小时超时和上行处理。 +10. 实现任务进度统计。 + +### 阶段 6:查询与统计 + +1. 实现客户端批量任务、发送详情、上行短信。 +2. 实现运营端任务进度、短信记录、上行记录。 +3. 实现客户端充值套餐、账单流水。 +4. 实现运营端充值记录、账务流水、套餐配置。 +5. 实现运营看板、发送监控、数据统计。 +6. 实现导出权限和导出日志。 + +### 阶段 7:性能与稳定性 + +1. 压测任务创建接口。 +2. 压测队列消费和发送 Worker。 +3. 压测回执处理。 +4. 验证 500 条/秒提交能力。 +5. 验证风控规则命中、审核原因、账务流水一致性。 +6. 验证通道限速、故障切换、重试、幂等。 +7. 加入监控告警和慢查询优化。 + +### 阶段 8:验收 + +1. 按原型完成页面功能联调。 +2. 完成核心流程端到端测试。 +3. 完成权限测试。 +4. 完成数据隔离测试。 +5. 完成账务对账测试。 +6. 完成性能测试报告。 +7. 完成 Linux 部署方案。 +8. 完成上线回滚方案。 + +## 13. Codex 开发执行建议 + +### 13.1 推荐任务拆分 + +1. 根据本文创建数据库迁移脚本和实体模型。 +2. 实现认证、租户、权限基础模块。 +3. 实现企业、应用、签名、模板 CRUD 与审核。 +4. 实现账户计费、套餐、账务流水。 +5. 实现通道、通道组、报备任务。 +6. 实现风控规则、审核原因、规则命中记录。 +7. 实现 NestJS Send Worker、BullMQ 队列和 Redis 限速。 +8. 实现 Go CMPP Gateway,并完成与 NestJS 队列事件的联调。 +9. 实现发送任务、发送明细、回执、上行。 +10. 前端将 mock service 替换为真实 API。 +11. 增加集成测试、性能测试和部署脚本。 + +### 13.2 给 Codex 的单任务提示模板 + +```text +请基于 docs/first-version-development-requirements.md,实现【模块名】。 +要求: +1. 遵循现有原型页面和字段命名。 +2. NestJS 后端实现 Prisma schema、migration、Service、Controller、DTO、单元测试。 +3. Go Gateway 实现配置、连接管理、CMPP submit/deliver/active test、回执事件发布、单元测试。 +4. 前端将对应 mock 数据替换为 API 调用,保留现有视觉样式。 +5. 补充错误处理、权限校验、操作日志。 +6. 运行构建和相关测试。 +``` + +## 14. 已确认关键决策 + +### 14.1 通道协议与报备 + +- 运营商通道协议参数和回执格式先按公开 CMPP 文档实现。 +- 签名报备字段模板由运营端自行配置和手工维护。 +- 第一期不考虑通过 API 向运营商传送签名报备资料。 + +### 14.2 发送能力 + +- 需要支持定时发送。 +- 导入号码文件格式支持 CSV、TXT。 +- 导入文件最大 20 MB。 +- 单任务最大号码数默认 100 万条。 + +### 14.3 风控默认阈值 + +- 重复号码比例默认阈值:30%。 +- 非法号码比例默认阈值:10%。 +- 黑名单命中比例默认阈值:5%。 +- 短时间任务创建频控默认阈值:同一企业/应用 10 分钟内 10 次。 + +### 14.4 计费与退费 + +- 最终失败、超时失败需要退费。 +- 计费口径可配置为按提交成功计费或按回执成功计费。 +- 短信计费规则只按 70/67 字拆分。 + +### 14.5 数据保存与更正 + +- 发送记录保存 12 个月。 +- 支持手动按月清除发送记录。 +- 冷热分层由技术方案自行设计。 +- 超时后供应商又返回成功回执时,不允许自动覆盖业务失败状态。 +- 系统需保留二次回执记录,并提供人工更正功能。 + +### 14.6 认证、部署与账号体系 + +- 第一版不考虑多租户独立域名和企业 SSO,只支持一个平台域名。 +- 部署环境为 Linux。 +- 最终需要给出 Linux 部署方案。 +- 企业认证资料采用人工上传、人工审核,不接第三方实名认证或营业执照 OCR。 + +### 14.7 待确认问题 + +暂无阻塞性待确认问题。后续进入详细设计或开发时,如遇具体运营商协议参数、生产部署资源规格、默认套餐价格等执行细节,再按模块补充确认。 + +## 15. 第一版落地执行路线 + +### 阶段 0:技术 Spike + +目标:优先验证最大技术风险,避免业务代码写完后发现网关链路不可用。 + +任务: + +1. Go CMPP Gateway Spike + - 基于 `gocmpp` 验证 CMPP connect、submit、deliver、active test、terminate。 + - 参考 `JoeCao/cmpp-gateway` 的连接、重连、SEQID/MSGID 追踪、模拟器设计。 + - 搭建最小 Go Gateway,支持配置一个模拟或真实 CMPP 通道。 + +2. BullMQ 通信 Spike + - NestJS 通过 Redis/BullMQ 下发发送指令。 + - Go Gateway 消费发送指令或监听队列事件。 + - Go Gateway 回传提交结果、回执、上行事件。 + +3. 500 条/秒链路 Spike + - 不接完整业务,只压测 NestJS 入队、Go Gateway 消费、模拟通道响应。 + - 验证平台可稳定入队并调度 500 条短信/秒。 + - 输出压测结果、瓶颈和下一步优化建议。 + +验收标准: + +- 能跑通一条模拟短信:NestJS 入队 -> Go Gateway 提交 -> submit resp -> deliver 回执 -> NestJS 更新状态。 +- Go Gateway 断线后可重连,并能继续消费后续消息。 +- 所有消息都有 messageId、channelId、sequenceId 或可追踪映射。 +- Spike 文档记录是否采用 `gocmpp`、是否需要 fork、哪些能力需要自研。 + +### 阶段 1:工程骨架 + +目标:让前端、NestJS API、Go Gateway 和基础设施能稳定启动。 + +任务: + +1. 建立 monorepo 或多目录结构: + - `web`:React + TypeScript + Vite + - `api`:NestJS + TypeScript + - `gateway`:Go CMPP Gateway + - `docs`:需求、设计、接口、部署文档 +2. 配置 PostgreSQL、Redis、MinIO、Docker Compose。 +3. 配置 Prisma、migration、seed。 +4. 配置 OpenAPI/Swagger。 +5. 配置日志、环境变量、健康检查。 +6. 配置基础 CI 命令:lint、test、build。 + +验收标准: + +- `web`、`api`、`gateway` 均可本地启动。 +- Docker Compose 可启动 PostgreSQL、Redis、MinIO。 +- NestJS 可连接 PostgreSQL 和 Redis。 +- Go Gateway 可读取配置并启动健康检查接口。 + +### 阶段 2:基础后台 + +目标:完成业务底座。 + +任务: + +1. 登录认证。 +2. 企业/租户。 +3. 用户、角色、权限。 +4. 操作日志。 +5. 文件上传。 +6. 系统字典。 +7. 手机号段库。 +8. 敏感词。 +9. 黑名单。 +10. 引流字段库。 + +验收标准: + +- 所有业务数据具备租户隔离。 +- 关键操作写入操作日志。 +- 文件上传可用于签名资料和认证资料。 + +### 阶段 3:短信配置 + +目标:客户和运营能配置发送短信所需资源。 + +任务: + +1. 短信应用。 +2. 应用密钥和 IP 白名单。 +3. 应用发送限额。 +4. 应用级“不符合模板短信”处理策略。 +5. 短信签名。 +6. 签名资料。 +7. 短信模板。 +8. 模板变量。 +9. 平台审核记录。 + +验收标准: + +- 客户端可创建应用、签名、模板。 +- 运营端可审核签名和模板。 +- 发送时只能使用审核通过的签名和模板。 +- 客户端不能修改已审核模板主体,只能填写变量。 + +### 阶段 4:通道与报备 + +目标:运营端能配置通道、通道组和签名报备流程。 + +任务: + +1. 短信通道管理。 +2. 通道组管理。 +3. 路由规则。 +4. 通道签名报备字段配置。 +5. 签名报备资料。 +6. 报备任务生成。 +7. 报备资料导出。 +8. 回执导入。 +9. 报备记录。 +10. 签名报备状态同步。 + +验收标准: + +- 签名审核状态和通道报备状态分层。 +- 发送前必须校验签名在目标通道报备通过。 +- 报备任务导出、导入具备幂等和历史记录。 + +### 阶段 5:账户计费 + +目标:发送前能判断是否可发,发送后能对账。 + +任务: + +1. 套餐配置。 +2. 充值套餐。 +3. 企业账户。 +4. 余额或套餐余量。 +5. 充值记录。 +6. 账单流水。 +7. 发送预估费用。 +8. 冻结、扣费、退费、解冻。 +9. 短信记录与账务流水关联。 + +验收标准: + +- 客户端充值套餐和账单流水进入第一版。 +- 发送前校验账户余额、套餐余量或授信额度。 +- 每条短信记录可追溯到账务流水。 + +### 阶段 6:风控与审核 + +目标:发送前有规则、有原因、有闭环。 + +任务: + +1. 风控规则配置。 +2. 默认阈值。 +3. 单任务最大号码数。 +4. 重复号码比例。 +5. 非法号码比例。 +6. 黑名单命中比例。 +7. 非工作时间大批量营销发送。 +8. 短时间任务创建频控。 +9. 模板变量异常。 +10. 风控命中记录。 +11. 短信审核。 +12. 审核原因展示。 +13. 拒绝原因返回客户端。 + +验收标准: + +- 命中风控规则时记录规则编号、规则名称、阈值、实际值、处理动作。 +- 进入审核的任务必须展示审核原因。 +- 直接拒绝的任务必须向客户端返回可读原因。 + +### 阶段 7:发送链路 + +目标:真实跑通短信发送。 + +任务: + +1. 客户端批量任务创建。 +2. 号码导入和拆分。 +3. 发送任务入队。 +4. Send Worker。 +5. 通道路由。 +6. Redis 限速。 +7. Go Gateway 提交 CMPP。 +8. Submit Resp 处理。 +9. 回执处理。 +10. 上行短信处理。 +11. 72 小时未知转超时。 +12. 任务进度统计。 + +验收标准: + +- 平台创建的批量任务只记录客户端任务。 +- 平台任务、API 调用、CMPP 对接发送全部按手机号维度进入短信记录。 +- 支持 submit resp、deliver 回执、上行短信、超时补偿。 + +### 阶段 8:查询、统计、验收 + +目标:系统可运营、可排查、可上线。 + +任务: + +1. 客户端批量任务。 +2. 客户端发送详情。 +3. 客户端上行短信。 +4. 运营端发送监控。 +5. 运营端任务进度。 +6. 运营端短信记录。 +7. 运营端上行记录。 +8. 运营看板。 +9. 数据统计。 +10. 操作日志审计。 +11. 性能压测。 +12. 500 条/秒报告。 +13. Linux 上线部署文档。 +14. 回滚方案。 + +验收标准: + +- 可按企业、应用、通道、任务、手机号追踪发送链路。 +- 可按账务流水和短信记录对账。 +- 输出 500 条/秒压测报告。 +- 输出 Linux 部署方案,至少覆盖 Docker Compose 或 systemd 部署、环境变量、数据库迁移、日志目录、备份恢复、服务健康检查和回滚步骤。 + +## 16. 新 Codex 会话提示词 + +建议新开 Codex 会话后直接发送以下提示词: + +```text +当前项目是 CMPP 短信平台第一版开发,工作区内已有前端设计原型和需求文档。 + +请先完整阅读: +1. docs/first-version-development-requirements.md +2. docs/ui-design-guidelines.md +3. package.json +4. src/routes/AppRoutes.tsx +5. src/layouts/ClientLayout.tsx +6. src/layouts/AdminLayout.tsx + +重要前提: +- 第一版保留短信业务,排除彩信功能。 +- 账户计费、充值套餐、账单流水进入第一版。 +- 前端使用 React + TypeScript + Vite。 +- 后端 API 使用 NestJS + TypeScript。 +- DB 使用 PostgreSQL。 +- ORM 使用 Prisma。 +- 队列使用 Redis + BullMQ。 +- 缓存/限速使用 Redis。 +- 文件使用 MinIO。 +- 第一版必须实现真实 Go CMPP Gateway。 +- Go Gateway 协议层优先评估 gocmpp,参考 JoeCao/cmpp-gateway 的连接、重连、SEQID/MSGID 追踪、模拟器设计。 +- 不从零手写整个 CMPP 协议栈,也不直接照搬完整开源网关;协议层可复用,服务层按本项目自研。 +- NestJS 负责业务审核、风控、计费、报备、路由和发送编排。 +- Go Gateway 只负责 CMPP 连接、协议提交、submit resp、回执、上行事件回传。 + +请从 docs/first-version-development-requirements.md 的“阶段 0:技术 Spike”开始执行。 + +第一步请先不要大规模写业务代码,先输出并创建阶段 0 Spike 的最小工程计划,包括: +1. 目录结构建议。 +2. NestJS 与 Go Gateway 的队列消息格式。 +3. Go Gateway 最小能力清单。 +4. gocmpp 与 cmpp-gateway 的技术评估任务。 +5. 500 条/秒 Spike 压测指标。 +6. 阶段 0 的验收标准。 + +然后按计划逐步实现。每完成一步都要运行构建或测试,并更新文档。 +``` diff --git a/docs/phase-0-open-source-evaluation.md b/docs/phase-0-open-source-evaluation.md new file mode 100644 index 0000000..b801247 --- /dev/null +++ b/docs/phase-0-open-source-evaluation.md @@ -0,0 +1,78 @@ +# 阶段 0 开源 CMPP 技术初评 + +核验日期:2026-07-01 + +## 初步结论 + +- `bigwhite/gocmpp` 更符合本项目“复用协议层、服务层自研”的方向,可作为第一优先协议库候选。 +- `JoeCao/cmpp-gateway` 更接近完整 HTTP 网关,不建议直接绑定业务模型;适合参考连接模型、重连、SEQID/MSGID 追踪、模拟器和降级设计。 +- 当前已完成 Go 1.26.4 下的 gocmpp 编译级接入、本地 TCP connect/submit/active test 测试和 deliver 回执 PDU 测试。 +- 阶段 0 决策:第一版协议层优先直接依赖 gocmpp,暂不 fork;服务层连接管理、重连、SEQID/MSGID 追踪、队列、限速、监控、幂等由本项目自研。 + +## bigwhite/gocmpp + +来源: + +- GitHub:https://github.com/bigwhite/gocmpp +- pkg.go.dev:https://pkg.go.dev/github.com/bigwhite/gocmpp + +### 已确认信息 + +- GitHub 标注 License 为 Apache-2.0。 +- 项目定位是 Go CMPP 协议库,可用于 client 和 server side。 +- README/pkg.go.dev 描述覆盖 CMPP 2.x 和 CMPP 3.x。 +- 已支持 connect、submit、deliver、fwd、active test、terminate。 +- query、cancel、route 等较少使用包未支持,且不在路线图中。 +- pkg.go.dev 可见基础连接 API,如 `Conn`、`SendPkt`、`RecvAndUnpackPkt`、`Server`。 +- 本地通过 `goproxy.cn` 成功解析 `github.com/bigwhite/gocmpp v0.0.0-20240917054108-b238366bff0b` 和 `golang.org/x/text v0.3.8`。 +- Gateway Spike 已创建 `internal/cmpp` 编译级适配测试,验证 `NewClient`、CMPP 2.0/3.0 类型映射可编译。 +- Gateway Spike 已创建本地 TCP 集成测试,验证 CMPP 3.0 connect、submit、submit resp、active test。 +- Gateway Spike 已创建 deliver 回执 pack/unpack 测试,验证 `DELIVRD` 状态报告可解析。 + +### 对本项目的价值 + +- 可降低从零实现 PDU 编解码、连接包、submit、deliver、active test、terminate 的风险。 +- 支持 client/server 双侧能力,有利于本地模拟 SMSC 和 Gateway 联调。 +- Apache-2.0 对商业项目相对友好,但仍需最终法务或项目负责人确认。 + +### 待实测问题 + +- Linux 编译情况。 +- 长短信拆分、UCS2、GBK/GB18030 编码稳定性。 +- submit resp 与 deliver 状态报告解析是否满足运营商实际格式。 +- 高并发 submit 下 sequence 管理、窗口控制和错误恢复能力。 +- 是否需要 fork 修补现代依赖、日志、context、超时、指标等工程化能力;阶段 0 暂不 fork。 + +## JoeCao/cmpp-gateway + +来源: + +- GitHub:https://github.com/JoeCao/cmpp-gateway + +### 已确认信息 + +- 项目定位是 CMPP 3.0 HTTP 网关,将 CMPP 协议转换为 HTTP API。 +- README 描述包含单连接多协程模型:Receiver、Sender、Heartbeat。 +- README 描述包含 SEQID 到 Message、MSGID 到 Message 的追踪链路。 +- README 描述内置心跳检测、断线重连、连接降级提示。 +- README 描述支持 BoltDB 和 Redis 做状态追踪。 +- README 描述包含本地 CMPP 模拟器,支持 CMPP 3.0 和 2.0、submit 成功响应、心跳保活和协议日志。 +- README 描述技术栈使用 Go 1.21+、gocmpp、Redis、标准库 net/http、GB18030。 + +### 对本项目的价值 + +- 可参考连接、重连、接收、发送和心跳协程的职责拆分。 +- 可参考 SEQID/MSGID 映射缓存设计,但本项目需改为以平台 `messageId` 为主键,和 NestJS/BullMQ 事件贯通。 +- 可参考模拟器设计,快速补齐 connect、submit resp、deliver、active test 和异常场景。 + +### 不建议直接采用的原因 + +- 它是完整 HTTP 网关,包含 HTTP API、Web UI、本地存储等服务层设计,和本项目“业务后台由 NestJS 负责,Go Gateway 只做 CMPP 连接和事件回传”的边界不一致。 +- 本项目需要 Redis/BullMQ 队列契约、通道级限速、业务后台路由和账务追踪,直接接入会形成业务模型耦合。 +- 第一版应避免直接照搬完整开源网关,服务层按项目自研。 + +## 阶段 0 后续实测清单 + +1. 在阶段 1 工程骨架中保留 `internal/cmpp` 适配层,避免业务代码直接散落调用 gocmpp。 +2. 阶段 7 实测断线重连、慢响应、窗口满、重复回执、sequence 回绕。 +3. 在 Linux 部署环境执行 `go test ./...` 和本地模拟 SMSC 联调。 diff --git a/docs/phase-0-spike-progress.md b/docs/phase-0-spike-progress.md new file mode 100644 index 0000000..8ed39c9 --- /dev/null +++ b/docs/phase-0-spike-progress.md @@ -0,0 +1,60 @@ +# 阶段 0 Spike 进度记录 + +## 2026-07-01 + +### 已完成 + +- 阅读第一版需求文档、UI 设计规范、前端路由、客户端布局、运营端布局和 package.json。 +- 确认第一版边界:保留短信业务,彩信为待开发或隐藏,账户计费进入第一版。 +- 创建阶段 0 最小工程计划。 +- 创建 NestJS 与 Go Gateway 队列消息 Schema 和示例消息。 +- 创建队列契约校验脚本。 +- 完成 `bigwhite/gocmpp` 与 `JoeCao/cmpp-gateway` 在线资料初评,记录到 `docs/phase-0-open-source-evaluation.md`。 +- 创建 `api/` 与 `gateway/` 阶段 0 README 骨架,明确 Spike 边界。 +- 安装 Go 1.26.4,补齐 Gateway Spike 可编译环境。 +- 创建 Go Gateway 队列消息结构、追踪器、内存模拟 submit resp/deliver 链路和 15000 条模拟压测命令。 +- 使用 `goproxy.cn` 成功接入 `github.com/bigwhite/gocmpp`,并创建编译级适配测试。 +- 安装 Redis Windows portable fork 8.8.0,启动本地 Redis 并通过 `PONG` 验证。 +- 安装 BullMQ/ioredis,创建 `api/src/spike/bullmq-link-spike.mjs`,跑通 15000 条 Redis/BullMQ 模拟链路。 +- 基于 gocmpp 创建本地 TCP 集成测试,覆盖 CMPP 3.0 connect、submit、submit resp、active test。 +- 基于 gocmpp 完成 deliver 回执 PDU pack/unpack 测试。 +- 创建 Gateway 重连状态机和测试,验证首次断线后第二次连接成功的重试路径。 + +### 验证记录 + +- `node --version`:v24.16.0。 +- `npm --version`:11.13.0。 +- `go version`:go1.26.4 windows/amd64。 +- `npm run spike:contracts`:通过,4 个队列消息示例通过校验。 +- `npm run build`:通过,Vite 输出 chunk 大小告警,不阻塞构建。 +- `npm run spike:gateway`:通过,包含 gocmpp 编译级接入、追踪器测试和内存模拟链路测试。 +- `go run ./cmd/spike`:通过,15000 条内存模拟链路全部生成 submit result 和 receipt event;本次运行耗时 32.8761 ms,吞吐 456258.50 msg/s。 +- Redis:通过本地 portable Redis 启动并返回 `PONG`。 +- `npm run spike:bullmq`:通过,15000 条消息;入队 3444.88 ms,端到端 24377.45 ms,入队 TPS 4354.29,端到端 TPS 615.32,满足 500 条/秒 Spike 指标。 +- `docker --version` / `docker compose version`:未通过,本机未安装 Docker。 + +### 当前阻塞与后续增强 + +- Docker 仍不可用;阶段 1 需要补齐 Docker Compose 或给出 Windows 本地替代启动方式。 +- gocmpp 已完成本地 TCP connect、submit、active test 和 deliver PDU 验证;terminate、慢响应、窗口满、重复回执、sequence 回绕需在阶段 7 稳定性压测继续补充。 +- 当前 BullMQ Spike 使用 Node 模拟 Gateway worker,不是 Go 进程直接消费 BullMQ;阶段 1/7 需要决定 Go 侧 Redis/BullMQ 兼容消费方式,或通过 NestJS Send Worker 将队列转为 Gateway 内部协议。 + +### 下一步 + +1. 进入阶段 1:工程骨架。 +2. 输出阶段 1 实施计划和验收标准。 +3. 建立 web/api/gateway/infra/docs 的工程结构与基础命令。 + +### 阶段 0 验收状态 + +| 验收项 | 状态 | 说明 | +| --- | --- | --- | +| 模拟短信链路:NestJS 入队 -> Go Gateway 提交 -> submit resp -> deliver 回执 -> NestJS 更新状态 | 已完成 | BullMQ/Redis 模拟链路已通过,Node 模拟 Gateway worker 回传 submit result 和 receipt event;Go 内存链路同步通过。 | +| Go Gateway 断线后可重连,并继续消费后续消息 | 已完成 | 已完成 Gateway 重连状态机测试;真实通道断线恢复将在阶段 7 加强。 | +| 全消息具备 traceId、messageId、channelId、sequenceId 或追踪映射 | 已完成 | Schema、示例、Go tracker 均已覆盖。 | +| Spike 文档记录是否采用 gocmpp、是否 fork、哪些能力自研 | 已完成 | 初步结论:协议层优先直接依赖 gocmpp,暂不 fork;连接管理、追踪、队列、限速、监控由本项目自研。 | +| 500 条/秒链路 Spike | 已完成 | BullMQ/Redis 端到端 TPS 615.32;Go 内存基线同步超过 500 条/秒。 | + +### 阶段 0 结论 + +阶段 0 已完成。第一版可以进入阶段 1 工程骨架建设。 diff --git a/docs/phase-0-technical-spike-plan.md b/docs/phase-0-technical-spike-plan.md new file mode 100644 index 0000000..9831d0c --- /dev/null +++ b/docs/phase-0-technical-spike-plan.md @@ -0,0 +1,160 @@ +# 阶段 0 技术 Spike 最小工程计划 + +## 目标 + +阶段 0 只验证第一版最大技术风险:NestJS 入队、Go Gateway 消费、模拟 CMPP 提交、submit resp、deliver 回执、上行事件回传、断线重连和 500 条/秒链路能力。 + +本阶段不实现完整业务审核、风控、计费、报备、路由后台,也不从零手写完整 CMPP 协议栈。 + +## 1. 目录结构建议 + +当前仓库先保留现有 React + TypeScript + Vite 原型作为根目录前端。阶段 0 建议以最小增量方式补齐后端和网关 Spike 目录: + +```text +. +├── docs/ +│ ├── contracts/ +│ │ ├── gateway-queue-messages.schema.json +│ │ └── examples/ +│ ├── phase-0-technical-spike-plan.md +│ └── phase-0-spike-progress.md +├── api/ +│ ├── README.md +│ └── src/ +│ └── spike/ +│ └── queue-contracts/ +├── gateway/ +│ ├── README.md +│ ├── cmd/ +│ │ ├── gateway/ +│ │ └── smsc-simulator/ +│ └── internal/ +│ ├── cmpp/ +│ ├── queue/ +│ ├── connection/ +│ ├── tracker/ +│ └── metrics/ +└── tools/ + └── spike/ + └── validate-gateway-queue-contract.mjs +``` + +阶段 1 再决定是否迁移为正式 monorepo: + +- `web/`:当前 React 原型。 +- `api/`:NestJS + Prisma + BullMQ。 +- `gateway/`:Go CMPP Gateway。 +- `infra/`:PostgreSQL、Redis、MinIO、Prometheus、Grafana 的 compose 与部署配置。 + +## 2. NestJS 与 Go Gateway 队列消息格式 + +阶段 0 采用 Redis + BullMQ 作为通信基线。NestJS 只负责投递已经完成业务校验和路由决策后的发送指令;Go Gateway 只负责协议提交和事件回传。 + +### 队列命名 + +- `cmpp.submit.commands`:NestJS -> Go Gateway,单号码发送指令。 +- `cmpp.submit.results`:Go Gateway -> NestJS,submit resp 结果。 +- `cmpp.receipt.events`:Go Gateway -> NestJS,deliver 回执事件。 +- `cmpp.uplink.events`:Go Gateway -> NestJS,上行短信事件。 + +### 统一信封 + +每条消息必须包含: + +- `schemaVersion`:当前固定为 `v1`。 +- `messageType`:消息类型。 +- `traceId`:贯穿 API、队列、网关、回执的链路 ID。 +- `messageId`:平台单号码短信 ID,全局唯一。 +- `channelId`:业务后台选定的通道 ID。 +- `createdAt`:ISO 8601 时间。 + +### 发送指令 + +`SubmitCommand` 由 NestJS 或 Send Worker 产生。必须包含 `tenantId`、`applicationId`、`submitId`、`phoneNumber`、`content`、`signature`、`templateId`、`billingUnits`、`route`、`cmpp` 和 `retry`。 + +Go Gateway 不根据签名、模板、账户余额做业务判断,只读取 `route.cmppAccountCode`、`cmpp.serviceId`、`cmpp.srcId`、`cmpp.registeredDelivery`、`cmpp.msgFmt` 等协议提交所需字段。 + +### 结果和事件 + +- `SubmitResult` 必须回传 `sequenceId`、`gatewayMessageId`、`submitStatus`、`submittedAt`。 +- `ReceiptEvent` 必须回传 `gatewayMessageId`、`sequenceId`、`receiptStatus`、`deliveredAt`、`rawStatus`。 +- `UplinkEvent` 必须回传 `phoneNumber`、`destId`、`content`、`receivedAt`、`sequenceId`。 + +详细结构以 `docs/contracts/gateway-queue-messages.schema.json` 为准。 + +## 3. Go Gateway 最小能力清单 + +阶段 0 最小 Go Gateway 只做以下能力: + +1. 读取一个通道配置:网关地址、端口、企业代码、账号、密码、接入号、CMPP 版本、窗口大小、心跳间隔、重连间隔。 +2. 基于 gocmpp 或评估后的协议库完成 connect、active test、submit、deliver、terminate。 +3. 消费 `cmpp.submit.commands`,提交到模拟 SMSC。 +4. 将 submit resp 写入 `cmpp.submit.results`。 +5. 将 deliver 回执写入 `cmpp.receipt.events`。 +6. 将上行短信写入 `cmpp.uplink.events`。 +7. 维护 `messageId -> sequenceId -> gatewayMessageId` 的追踪映射。 +8. 支持断线重连,重连后继续消费后续消息。 +9. 暴露最小健康检查和指标:连接状态、队列积压、提交 TPS、submit 成功率、回执延迟、重连次数。 +10. 支持模拟 SMSC:正常响应、慢响应、断线、重复回执、窗口满。 + +## 4. gocmpp 与 cmpp-gateway 技术评估任务 + +评估对象: + +- `bigwhite/gocmpp`:优先评估为协议层依赖。 +- `JoeCao/cmpp-gateway`:只作为连接管理、重连、SEQID/MSGID 追踪和模拟器设计参考。 + +任务清单: + +1. License:确认依赖许可是否允许第一版商业项目使用。 +2. 维护活跃度:确认最近提交、Issue、PR、Go module 支持情况。 +3. 协议覆盖:确认 CMPP 2.0/3.0、connect、submit、deliver、active test、terminate 支持情况。 +4. 编解码可靠性:验证长短信、UCS2、GBK、状态报告、上行短信解析。 +5. 连接模型:评估长连接、心跳、窗口、并发 submit、断线重连。 +6. 追踪能力:验证 sequenceId、msgId、平台 messageId 的映射方案。 +7. 模拟器:复用或参考模拟 SMSC 的响应、慢响应、断线和重复回执能力。 +8. 改造边界:判断直接依赖、轻量 fork、或只参考实现的取舍。 +9. 风险清单:列出协议层缺口、生产部署风险、测试补充项。 + +## 5. 500 条/秒 Spike 压测指标 + +压测只验证链路调度能力,不验证完整业务规则。 + +### 场景 + +1. NestJS 批量投递 30 秒,共 15000 条发送指令。 +2. Go Gateway 消费队列并提交到模拟 SMSC。 +3. 模拟 SMSC 立即返回 submit resp,并在 1 到 3 秒内返回 deliver。 +4. 重复执行单连接和多连接两个场景。 +5. 插入异常场景:模拟 SMSC 慢响应、短暂断线、重复回执。 + +### 指标 + +- 入队吞吐:P50、P95、P99,每秒入队数必须稳定达到 500。 +- 消费吞吐:每秒消费并提交数,30 秒平均不低于 500。 +- 端到端延迟:入队到 submit resp P95 小于 2 秒。 +- 回执延迟:deliver 到 NestJS 消费 P95 小于 10 秒。 +- 错误率:submit command 处理失败率小于 0.1%。 +- 重试率:异常场景下可观测且不造成重复最终状态。 +- 队列积压:压测停止后 30 秒内归零。 +- 资源指标:CPU、内存、Redis ops、连接重连次数。 +- 幂等指标:重复回执只产生一条最终状态更新,多余回执进入历史记录。 + +## 6. 阶段 0 验收标准 + +1. 跑通一条模拟短信链路:NestJS 入队 -> Go Gateway 提交 -> submit resp -> deliver 回执 -> NestJS 更新状态。 +2. Go Gateway 断线后可重连,并能继续消费后续消息。 +3. 所有消息具备 `traceId`、`messageId`、`channelId`、`sequenceId` 或可追踪映射。 +4. 队列消息 Schema、示例消息和校验脚本通过。 +5. 完成 gocmpp 与 JoeCao/cmpp-gateway 评估记录,明确最终依赖方式。 +6. 完成 500 条/秒压测报告,包含瓶颈、资源使用和下一阶段优化建议。 +7. 文档记录 Go Gateway 哪些能力复用协议库,哪些服务层能力由本项目自研。 + +## 阶段 0 执行顺序 + +1. 创建阶段 0 计划文档、队列消息 Schema、示例消息和校验脚本。 +2. 创建 Go Gateway Spike 骨架和模拟 SMSC 骨架。 +3. 创建 NestJS Spike 骨架,完成 BullMQ 入队和事件消费。 +4. 接入 Redis 本地环境,跑通模拟链路。 +5. 评估 gocmpp 与 JoeCao/cmpp-gateway。 +6. 完成 500 条/秒压测脚本和报告。 diff --git a/docs/phase-1-engineering-skeleton-plan.md b/docs/phase-1-engineering-skeleton-plan.md new file mode 100644 index 0000000..adfa730 --- /dev/null +++ b/docs/phase-1-engineering-skeleton-plan.md @@ -0,0 +1,55 @@ +# 阶段 1 工程骨架实施计划 + +## 目标 + +让前端原型、NestJS API、Go CMPP Gateway 和基础设施具备稳定启动、构建、测试和文档入口,为阶段 2 到阶段 8 的业务开发提供工程底座。 + +## 实施步骤 + +1. 文档与阶段门 + - 创建阶段 1 计划和进度记录。 + - 明确阶段 1 不实现完整业务,只建立工程骨架。 + +2. 前端工程边界 + - 当前 React + TypeScript + Vite 原型暂时保留在仓库根目录。 + - 第一版继续保留短信业务和账户计费入口,彩信入口保持待开发或后续隐藏。 + - 后续如迁移到 `web/`,需单独做无行为变化迁移。 + +3. NestJS API 骨架 + - 创建 `api/package.json`、`tsconfig.json`、`src/main.ts`、`src/app.module.ts`。 + - 配置健康检查接口。 + - 配置 Swagger/OpenAPI 文档入口。 + - 配置环境变量读取。 + - 保留阶段 0 BullMQ Spike 脚本。 + +4. Prisma 与数据库流程 + - 创建 `api/prisma/schema.prisma`。 + - 建立 `prisma generate` 和迁移命令入口。 + - 阶段 1 只定义基础连接和首批占位模型,不展开完整业务表。 + +5. Go Gateway 骨架 + - 保留阶段 0 gocmpp 适配、追踪器、重连状态机和 Spike 测试。 + - 增加健康检查 HTTP server 入口。 + - 明确 Gateway 只做 CMPP 连接、提交、submit resp、回执、上行事件回传。 + +6. 基础设施 + - 创建 `infra/docker-compose.yml`,覆盖 PostgreSQL、Redis、MinIO。 + - 创建 `.env.example`,列出 web/api/gateway/infra 的必要变量。 + - 当前 Windows 本地可用 Redis portable;Linux/容器环境以 Docker Compose 为准。 + +7. 统一验证命令 + - 根目录保留前端 `npm run build`。 + - 增加 API 构建命令。 + - 保留 `npm run spike:contracts`、`npm run spike:bullmq`、`npm run spike:gateway`。 + - 阶段 1 完成时执行全套验证。 + +## 验收标准 + +- 前端:`npm run build` 通过。 +- API:`cd api && npm run build` 通过,健康检查和 Swagger 入口代码存在。 +- Gateway:`npm run spike:gateway` 通过。 +- 队列契约:`npm run spike:contracts` 通过。 +- BullMQ Spike:`npm run spike:bullmq` 通过。 +- 基础设施:`infra/docker-compose.yml` 包含 PostgreSQL、Redis、MinIO。 +- Prisma:`api/prisma/schema.prisma` 存在,API package 提供 `prisma:generate` 和迁移命令。 +- 文档:`docs/phase-1-engineering-skeleton-progress.md` 记录每一步验证结果。 diff --git a/docs/phase-1-engineering-skeleton-progress.md b/docs/phase-1-engineering-skeleton-progress.md new file mode 100644 index 0000000..6084940 --- /dev/null +++ b/docs/phase-1-engineering-skeleton-progress.md @@ -0,0 +1,53 @@ +# 阶段 1 工程骨架进度记录 + +## 2026-07-01 + +### 阶段计划 + +- 已创建 `docs/phase-1-engineering-skeleton-plan.md`。 +- 阶段 1 范围限定为工程骨架,不展开完整业务实现。 + +### 验证记录 + +- 创建阶段 1 计划和进度文档后执行: + - `npm run spike:contracts`:通过。 + - `npm run spike:gateway`:通过。 + - `npm run build`:通过,仍有 Vite chunk 大小告警,不阻塞。 +- 创建 NestJS API 骨架后执行: + - `cd api && npm install`:完成,npm audit 报 7 个漏洞(3 moderate、4 high),暂未执行 `npm audit fix --force` 以避免破坏依赖版本。 + - `cd api && npm run build`:首次因 TypeScript 6 配置要求失败,补充 `rootDir` 和 `ignoreDeprecations` 后通过。 +- 创建基础设施和 Gateway 健康检查后执行: + - `npm run spike:gateway`:通过,包含 Gateway health handler 测试。 + - `cd api && npm run build`:通过。 + - `npm run build`:通过,仍有 Vite chunk 大小告警,不阻塞。 +- 迁移 Prisma 7 配置后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 +- 阶段 1 完整验证: + - `npm run verify:phase1`:通过。 + - BullMQ 本轮端到端 TPS:948.32,满足 500 条/秒 Spike 验证线。 + - Prisma Client 生成成功。 + - API 构建成功。 + - 前端构建成功,仍有 Vite chunk 大小告警,不阻塞。 + +### 阶段 1 验收状态 + +| 验收项 | 状态 | 说明 | +| --- | --- | --- | +| 前端 `npm run build` 通过 | 已完成 | 当前 React + Vite 原型构建通过。 | +| API `cd api && npm run build` 通过 | 已完成 | NestJS health/Swagger 骨架可编译。 | +| Gateway `npm run spike:gateway` 通过 | 已完成 | gocmpp、重连、health、追踪器测试通过。 | +| 队列契约 `npm run spike:contracts` 通过 | 已完成 | 4 个示例通过校验。 | +| BullMQ Spike `npm run spike:bullmq` 通过 | 已完成 | 15000 条端到端 TPS 948.32。 | +| 基础设施 compose 覆盖 PostgreSQL、Redis、MinIO | 已完成 | `infra/docker-compose.yml` 已创建;本机暂无 Docker,未执行 compose up。 | +| Prisma schema 和命令入口存在 | 已完成 | `api/prisma/schema.prisma`、`api/prisma.config.ts` 和 npm scripts 已创建。 | +| 文档记录阶段 1 进度 | 已完成 | 本文件已记录。 | + +### 阶段 1 结论 + +阶段 1 已完成。第一版可以进入阶段 2 基础后台。 + +### 下一步 + +1. 输出阶段 2 实施计划和验收标准。 +2. 按阶段 2 建立认证、租户、用户、角色、权限、操作日志、文件、基础字典和账户计费基础模型。 diff --git a/docs/phase-2-foundation-backend-plan.md b/docs/phase-2-foundation-backend-plan.md new file mode 100644 index 0000000..9395f7e --- /dev/null +++ b/docs/phase-2-foundation-backend-plan.md @@ -0,0 +1,38 @@ +# 阶段 2 基础后台实施计划 + +## 目标 + +完成第一版业务底座:认证、租户隔离、用户/角色/权限、操作日志、文件上传元数据、基础字典、手机号段、敏感词、黑名单、引流字段、账户/套餐/账务流水基础模型。 + +阶段 2 优先建立后端模型与模块边界,不展开完整页面联调。 + +## 实施步骤 + +1. Prisma 基础模型 + - 租户、用户、角色、权限、用户角色、角色权限。 + - 操作日志。 + - 文件对象元数据。 + - 手机号段、敏感词、企业黑名单、全局黑名单、引流字段。 + - 账户、套餐、账务流水、计费规则。 + +2. NestJS 模块边界 + - `AuthModule` + - `TenantsModule` + - `UsersModule` + - `AuditModule` + - `FilesModule` + - `DictionariesModule` + - `BillingModule` + +3. 验证 + - `npm --prefix api run prisma:generate` + - `npm --prefix api run build` + - `npm run verify:phase1` + +## 验收标准 + +- Prisma Client 生成成功。 +- API 构建成功。 +- 基础模型覆盖阶段 2 范围。 +- NestJS 模块边界存在并被 AppModule 引入。 +- 文档记录阶段 2 进度和验证结果。 diff --git a/docs/phase-2-foundation-backend-progress.md b/docs/phase-2-foundation-backend-progress.md new file mode 100644 index 0000000..18846d9 --- /dev/null +++ b/docs/phase-2-foundation-backend-progress.md @@ -0,0 +1,107 @@ +# 阶段 2 基础后台进度记录 + +## 2026-07-01 + +### 阶段计划 + +- 已创建 `docs/phase-2-foundation-backend-plan.md`。 +- 阶段 2 第一轮聚焦后端模型和模块边界。 + +### 验证记录 + +- 扩展 Prisma 基础模型并创建 NestJS 模块边界后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 +- 阶段回归验证: + - `npm run verify:phase1`:通过。 + - BullMQ 本轮端到端 TPS:1019.27,满足 500 条/秒验证线。 + - Prisma Client 生成成功。 + - API 构建成功。 + - 前端构建成功,仍有 Vite chunk 大小告警,不阻塞。 +- 实现基础后台最小 API 后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 + - API health 冒烟:`GET http://127.0.0.1:3101/api/health` 返回 `{"status":"ok","service":"cmpp-platform-api",...}`。 +- 阶段 2 完整验证: + - `npm run verify:phase2`:通过。 + - BullMQ 本轮端到端 TPS:531.54,满足 500 条/秒验证线。 + - Prisma Client 生成成功。 + - API 构建成功。 + - 前端构建成功,仍有 Vite chunk 大小告警,不阻塞。 + +### 已完成 + +- Prisma 基础模型: + - `Tenant` + - `User` + - `Role` + - `Permission` + - `UserRole` + - `RolePermission` + - `OperationLog` + - `FileObject` + - `PhoneSegment` + - `SensitiveWord` + - `EnterpriseBlacklist` + - `GlobalBlacklist` + - `DrainageField` + - `BillingPlan` + - `TenantAccount` + - `AccountTransaction` + - `BillingRule` +- NestJS 模块边界: + - `AuthModule` + - `TenantsModule` + - `UsersModule` + - `AuditModule` + - `FilesModule` + - `DictionariesModule` + - `BillingModule` +- 最小 API 能力: + - `POST /api/client/auth/login` + - `GET/POST /api/admin/tenants` + - `GET/POST /api/admin/users` + - `GET/POST /api/admin/users/roles` + - `GET/POST /api/admin/users/permissions` + - `POST /api/admin/users/roles/assign` + - `POST /api/admin/users/permissions/assign` + - `GET/POST /api/admin/operation-logs` + - `GET/POST /api/admin/files` + - `POST /api/admin/files/presigned-upload` + - `GET/POST /api/admin/dictionaries/phone-segments` + - `GET/POST /api/admin/dictionaries/sensitive-words` + - `GET/POST /api/admin/dictionaries/blacklists/global` + - `GET/POST /api/admin/dictionaries/blacklists/enterprise` + - `GET/POST /api/admin/dictionaries/drainage-fields` + - `GET/POST /api/admin/billing/plans` + - `GET/POST /api/admin/billing/accounts` + - `GET/POST /api/admin/billing/transactions` + - `GET/POST /api/admin/billing/rules` + - `GET /api/client/billing/plans` + - `GET /api/client/billing/transactions` +- 基础设施: + - Prisma 7 使用 `@prisma/adapter-pg`。 + - 文件上传使用 MinIO SDK 生成预签名上传 URL。 + - `PrismaService` 懒连接,允许无 PostgreSQL 时启动 health/Swagger。 + +### 阶段 2 验收状态 + +| 验收项 | 状态 | 说明 | +| --- | --- | --- | +| 登录认证基础接口 | 已完成 | 已有用户名/密码登录占位,返回开发 token;正式 JWT/密码算法阶段后续加强。 | +| 企业/租户基础能力 | 已完成 | 租户列表、详情、创建接口已实现。 | +| 用户、角色、权限 | 已完成 | 用户、角色、权限、分配关系接口已实现。 | +| 操作日志 | 已完成 | 操作日志列表和创建接口已实现。 | +| 文件上传和对象存储 | 已完成 | 文件元数据接口和 MinIO 预签名上传入口已实现。 | +| 基础字典 | 已完成 | 手机号段、敏感词、黑名单、引流字段接口已实现。 | +| 账户、套餐、账务流水基础模型 | 已完成 | 套餐、账户、流水、计费规则接口已实现,客户端账单查询入口已提供。 | +| 构建/生成验证 | 已完成 | Prisma generate、API build、API health 冒烟均通过。 | + +### 阶段 2 结论 + +阶段 2 已完成。第一版可以进入阶段 3 短信配置。 + +### 下一步 + +1. 输出阶段 3 实施计划和验收标准。 +2. 实现短信应用、签名、签名资料、模板、模板变量、审核记录基础模型和接口。 diff --git a/docs/phase-3-sms-configuration-plan.md b/docs/phase-3-sms-configuration-plan.md new file mode 100644 index 0000000..bdc969b --- /dev/null +++ b/docs/phase-3-sms-configuration-plan.md @@ -0,0 +1,41 @@ +# 阶段 3 短信配置实施计划 + +## 目标 + +让客户和运营能配置发送短信所需资源:短信应用、应用密钥和 IP 白名单、应用发送限额、不符合模板短信处理策略、短信签名、签名资料、短信模板、模板变量、审核记录和审核工作台基础接口。 + +## 实施步骤 + +1. Prisma 模型 + - `SmsApplication` + - `SmsApplicationIpAllowlist` + - `SmsSignature` + - `SignatureMaterial` + - `SmsTemplate` + - `TemplateVariable` + - `AuditRecord` + +2. NestJS 模块 + - `SmsConfigModule` + - 客户端接口:应用、签名、模板创建/查询/提交审核。 + - 运营端接口:企业应用/签名/模板查询,签名/模板审核通过或驳回。 + +3. 规则约束 + - 应用、签名、模板均按 `tenantId` 隔离。 + - 已审核模板主体第一版不允许直接修改;阶段 3 先不实现编辑接口,后续如需要编辑只允许草稿/驳回状态。 + - 签名和模板提交审核时写入 `AuditRecord`。 + +4. 验证 + - `cd api && npm run prisma:generate` + - `cd api && npm run build` + - API health 冒烟 + - `npm run verify:phase2` + +## 验收标准 + +- Prisma Client 生成成功。 +- API 构建成功。 +- `SmsConfigModule` 被 AppModule 引入。 +- 客户端短信应用、签名、模板基础接口存在。 +- 运营端短信应用、签名、模板查询和审核接口存在。 +- 阶段 3 进度文档记录验证结果。 diff --git a/docs/phase-3-sms-configuration-progress.md b/docs/phase-3-sms-configuration-progress.md new file mode 100644 index 0000000..f75f644 --- /dev/null +++ b/docs/phase-3-sms-configuration-progress.md @@ -0,0 +1,81 @@ +# 阶段 3 短信配置进度记录 + +## 2026-07-01 + +### 阶段计划 + +- 已创建 `docs/phase-3-sms-configuration-plan.md`。 +- 阶段 3 聚焦短信配置域,不开发彩信功能。 + +### 验证记录 + +- 创建阶段 3 计划文档、扩展 Prisma 短信配置模型后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 +- 实现 `SmsConfigModule`、客户端/运营端最小接口后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 + - API health 冒烟:`GET http://127.0.0.1:3101/api/health` 返回 `{"status":"ok","service":"cmpp-platform-api",...}`。 +- 阶段 3 完整验证: + - `npm run verify:phase3`:通过。 + - BullMQ 本轮端到端 TPS:548.52,满足 500 条/秒验证线。 + - Prisma Client 生成成功。 + - API 构建成功。 + - 前端构建成功,仍有 Vite chunk 大小告警,不阻塞。 + +### 已完成 + +- Prisma 短信配置模型: + - `SmsApplication` + - `SmsApplicationIpAllowlist` + - `SmsSignature` + - `SignatureMaterial` + - `SmsTemplate` + - `TemplateVariable` + - `AuditRecord` +- NestJS 模块: + - `SmsConfigModule` + - `SmsConfigService` + - `ClientSmsConfigController` + - `AdminSmsConfigController` +- 客户端接口: + - `GET/POST /api/client/applications` + - `GET/POST /api/client/signatures` + - `POST /api/client/signatures/:id/materials` + - `POST /api/client/signatures/:id/submit` + - `GET/POST /api/client/templates` + - `POST /api/client/templates/:id/submit` +- 运营端接口: + - `GET /api/admin/enterprise-applications` + - `GET /api/admin/enterprise-signatures` + - `GET /api/admin/enterprise-templates` + - `GET /api/admin/audit-records` + - `POST /api/admin/signatures/:id/approve` + - `POST /api/admin/signatures/:id/reject` + - `POST /api/admin/templates/:id/approve` + - `POST /api/admin/templates/:id/reject` +- 规则: + - 签名/模板提交审核会写入 `AuditRecord`。 + - 审核通过/驳回会更新审核状态并写入 `AuditRecord`。 + - 模板变量支持显式传入,也支持从 `${变量名}` 自动识别。 + - 短信计费条数按 70/67 字规则预估。 + +### 下一步 + +1. 输出阶段 4 实施计划和验收标准。 +2. 实现短信通道、通道组、路由规则、报备字段、签名报备资料、报备任务、导出/导入记录和报备状态同步。 + +### 阶段 3 验收状态 + +| 验收项 | 状态 | 说明 | +| --- | --- | --- | +| Prisma Client 生成成功 | 已完成 | `npm run prisma:generate` 通过。 | +| API 构建成功 | 已完成 | `npm run build` 在 `api` 下通过。 | +| `SmsConfigModule` 被 AppModule 引入 | 已完成 | 已引入根模块。 | +| 客户端短信应用、签名、模板基础接口存在 | 已完成 | 应用/签名/模板查询、创建、提交审核接口已实现。 | +| 运营端短信应用、签名、模板查询和审核接口存在 | 已完成 | 企业应用/签名/模板查询,签名/模板通过/驳回接口已实现。 | +| 阶段完整验证 | 已完成 | `npm run verify:phase3` 通过。 | + +### 阶段 3 结论 + +阶段 3 已完成。第一版可以进入阶段 4 通道与报备。 diff --git a/docs/phase-4-channel-reporting-plan.md b/docs/phase-4-channel-reporting-plan.md new file mode 100644 index 0000000..0fc0e6c --- /dev/null +++ b/docs/phase-4-channel-reporting-plan.md @@ -0,0 +1,51 @@ +# 阶段 4 通道与报备实施计划 + +## 目标 + +让运营端能配置短信通道、通道组、路由规则和签名报备流程;让系统能记录签名在各通道的报备资料、报备任务、导出文件、回执导入和状态同步。 + +阶段 4 不实现真实发送链路,真实 CMPP 提交在阶段 7 发送链路接入;本阶段只建立通道和报备配置闭环。 + +## 实施步骤 + +1. Prisma 模型 + - `SmsChannel` + - `SmsChannelGroup` + - `SmsChannelGroupItem` + - `ChannelRouteRule` + - `ChannelHealthMetric` + - `ChannelReportField` + - `SignatureReportMaterial` + - `ChannelSignatureReportTask` + - `ChannelSignatureReportRecord` + - `ReportExportFile` + - `ReportReceiptImport` + +2. NestJS 模块 + - `ChannelsModule` + - 通道 CRUD、测试短信占位、通道健康指标查询。 + - 通道组 CRUD、通道组成员配置。 + - 路由规则 CRUD。 + - 报备字段配置 CRUD。 + - 签名报备资料登记。 + - 报备任务生成、导出记录、回执导入记录、报备记录查询。 + +3. 状态同步 + - 生成报备任务时记录 `pending`。 + - 导出报备资料时记录导出文件和报备记录。 + - 导入回执时记录导入批次、回执结果,并更新任务状态。 + +4. 验证 + - `cd api && npm run prisma:generate` + - `cd api && npm run build` + - API health 冒烟 + - `npm run verify:phase3` + +## 验收标准 + +- Prisma Client 生成成功。 +- API 构建成功。 +- `ChannelsModule` 被 AppModule 引入。 +- 通道、通道组、路由规则、报备字段、报备任务基础接口存在。 +- 报备导出/导入记录和报备状态同步接口存在。 +- 阶段 4 进度文档记录验证结果。 diff --git a/docs/phase-4-channel-reporting-progress.md b/docs/phase-4-channel-reporting-progress.md new file mode 100644 index 0000000..07d8d35 --- /dev/null +++ b/docs/phase-4-channel-reporting-progress.md @@ -0,0 +1,83 @@ +# 阶段 4 通道与报备进度记录 + +## 2026-07-01 + +### 阶段计划 + +- 已创建 `docs/phase-4-channel-reporting-plan.md`。 +- 阶段 4 聚焦短信通道和签名报备,不开发彩信通道。 + +### 验证记录 + +- 创建阶段 4 计划文档、扩展 Prisma 通道与报备模型后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 +- 实现 `ChannelsModule`、通道/通道组/路由/报备最小接口后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 + - API health 冒烟:`GET http://127.0.0.1:3101/api/health` 返回 `{"status":"ok","service":"cmpp-platform-api",...}`。 +- 阶段 4 完整验证: + - `npm run verify:phase4`:通过。 + - BullMQ 本轮端到端 TPS:527.63,满足 500 条/秒验证线。 + - Prisma Client 生成成功。 + - API 构建成功。 + - 前端构建成功,仍有 Vite chunk 大小告警,不阻塞。 + +### 已完成 + +- Prisma 通道与报备模型: + - `SmsChannel` + - `SmsChannelGroup` + - `SmsChannelGroupItem` + - `ChannelRouteRule` + - `ChannelHealthMetric` + - `ChannelReportField` + - `SignatureReportMaterial` + - `ChannelSignatureReportTask` + - `ChannelSignatureReportRecord` + - `ReportExportFile` + - `ReportReceiptImport` +- NestJS 模块: + - `ChannelsModule` + - `ChannelsService` + - `ChannelsController` +- 运营端接口: + - `GET/POST /api/admin/channels` + - `POST /api/admin/channels/:id/test` + - `GET /api/admin/channels/:id/metrics` + - `GET/POST /api/admin/channel-groups` + - `POST /api/admin/channel-groups/items` + - `GET/POST /api/admin/channel-route-rules` + - `GET/POST /api/admin/channel-report-fields` + - `GET/POST /api/admin/signature-report-materials` + - `GET /api/admin/report-tasks` + - `POST /api/admin/report-tasks/generate` + - `POST /api/admin/report-tasks/:id/export` + - `POST /api/admin/report-tasks/:id/receipt-import` + - `GET /api/admin/report-records` +- 报备状态规则: + - 生成报备任务时创建 `pending` 任务和报备记录。 + - 导出报备资料时创建导出文件记录,并将任务状态更新为 `exporting`。 + - 导入回执时创建导入批次,按结果更新任务状态,并同步签名 `reportStatus`。 + +### 下一步 + +1. 输出阶段 5 实施计划和验收标准。 +2. 实现账户计费完整扣费/冻结/退费闭环、套餐购买/充值记录和发送前费用预估。 + +### 阶段 4 验收状态 + +| 验收项 | 状态 | 说明 | +| --- | --- | --- | +| Prisma Client 生成成功 | 已完成 | `npm run prisma:generate` 通过。 | +| API 构建成功 | 已完成 | `npm run build` 在 `api` 下通过。 | +| `ChannelsModule` 被 AppModule 引入 | 已完成 | 已引入根模块。 | +| 通道、通道组、路由规则基础接口存在 | 已完成 | 通道 CRUD、测试占位、指标查询、通道组、成员和路由规则接口已实现。 | +| 报备字段和签名报备资料接口存在 | 已完成 | 报备字段配置、签名通道报备资料登记接口已实现。 | +| 报备任务、导出、导入和记录接口存在 | 已完成 | 任务生成、导出文件、回执导入、报备记录查询接口已实现。 | +| 报备状态同步 | 已完成 | 回执导入后更新任务状态并同步签名 `reportStatus`。 | +| 阶段完整验证 | 已完成 | `npm run verify:phase4` 通过。 | + +### 阶段 4 结论 + +阶段 4 已完成。第一版可以进入阶段 5 账户计费。 diff --git a/docs/phase-5-billing-plan.md b/docs/phase-5-billing-plan.md new file mode 100644 index 0000000..79a8a34 --- /dev/null +++ b/docs/phase-5-billing-plan.md @@ -0,0 +1,39 @@ +# 阶段 5 账户计费实施计划 + +## 目标 + +实现发送前可判断是否可发,发送后可对账的账户计费基础闭环:套餐配置、充值套餐、企业账户、余额或套餐余量、充值记录、账单流水、发送预估费用、冻结、扣费、退费、解冻、短信记录与账务流水关联。 + +阶段 5 不实现完整发送链路;发送记录正式关联会在阶段 7 发送链路落地时接入。本阶段先提供 `relatedType`/`relatedId` 关联能力和费用预估/账务动作 API。 + +## 实施步骤 + +1. Prisma 模型补强 + - `RechargeOrder` + - `SmsBillingRecord` + - 扩展账务流水关联字段。 + +2. Billing 服务动作 + - 套餐购买/人工充值。 + - 发送费用预估。 + - 账户可用额度检查。 + - 冻结、扣费、解冻、退费。 + - 账务流水关联任务、短信记录或人工单据。 + +3. API + - 客户端充值套餐、充值订单、账单流水、费用预估。 + - 运营端充值记录、账务流水、套餐配置、计费规则、人工充值和账务调整。 + +4. 验证 + - `cd api && npm run prisma:generate` + - `cd api && npm run build` + - API health 冒烟 + - `npm run verify:phase4` + +## 验收标准 + +- Prisma Client 生成成功。 +- API 构建成功。 +- 套餐、充值记录、账务流水、计费规则 API 存在。 +- 费用预估、冻结、扣费、解冻、退费 API 存在。 +- 阶段 5 进度文档记录验证结果。 diff --git a/docs/phase-5-billing-progress.md b/docs/phase-5-billing-progress.md new file mode 100644 index 0000000..688c91b --- /dev/null +++ b/docs/phase-5-billing-progress.md @@ -0,0 +1,75 @@ +# 阶段 5 账户计费进度记录 + +## 2026-07-01 + +### 阶段计划 + +- 已创建 `docs/phase-5-billing-plan.md`。 +- 阶段 5 聚焦账户计费闭环,不实现完整发送链路。 + +### 验证记录 + +- 创建阶段 5 计划文档、扩展 Prisma 充值订单和短信计费记录模型后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 +- 实现 Billing 账务动作 API 后执行: + - `cd api && npm run prisma:generate`:通过。 + - `cd api && npm run build`:通过。 + - API health 冒烟:`GET http://127.0.0.1:3101/api/health` 返回 `{"status":"ok","service":"cmpp-platform-api",...}`。 +- 阶段 5 完整验证: + - `npm run verify:phase5`:通过。 + - BullMQ 本轮端到端 TPS:543.91,满足 500 条/秒验证线。 + - Prisma Client 生成成功。 + - API 构建成功。 + - 前端构建成功,仍有 Vite chunk 大小告警,不阻塞。 + +### 已完成 + +- Prisma 账户计费模型补强: + - `RechargeOrder` + - `SmsBillingRecord` +- Billing 账务动作: + - 套餐购买/人工充值:创建 `RechargeOrder` 并写入 `AccountTransaction`。 + - 发送费用预估:按 70/67 字规则计算计费条数、总条数和金额。 + - 账户校验:检查余额+授信额度和套餐余量。 + - 冻结:写入 `frozen` 流水。 + - 扣费:写入 `charged` 流水。 + - 解冻:写入 `released` 流水。 + - 退费:写入 `refunded` 流水。 + - 调整:写入 `adjusted` 流水。 + - 短信计费记录:创建和查询 `SmsBillingRecord`。 +- 运营端接口: + - `GET/POST /api/admin/billing/recharges` + - `POST /api/admin/billing/estimate` + - `POST /api/admin/billing/check` + - `POST /api/admin/billing/freeze` + - `POST /api/admin/billing/charge` + - `POST /api/admin/billing/release` + - `POST /api/admin/billing/refund` + - `POST /api/admin/billing/adjust` + - `GET/POST /api/admin/billing/sms-billing-records` +- 客户端接口: + - `GET/POST /api/client/billing/orders` + - `POST /api/client/billing/estimate` + +### 下一步 + +1. 输出阶段 6 实施计划和验收标准。 +2. 实现风控规则、默认阈值、规则命中记录、短信审核、审核原因展示和拒绝原因返回。 + +### 阶段 5 验收状态 + +| 验收项 | 状态 | 说明 | +| --- | --- | --- | +| Prisma Client 生成成功 | 已完成 | `npm run prisma:generate` 通过。 | +| API 构建成功 | 已完成 | `npm run build` 在 `api` 下通过。 | +| 套餐、充值记录、账务流水、计费规则 API 存在 | 已完成 | 已有套餐、充值订单、账户、流水、规则接口。 | +| 费用预估 API 存在 | 已完成 | `POST /api/admin/billing/estimate` 与 `POST /api/client/billing/estimate` 已实现。 | +| 账户校验 API 存在 | 已完成 | `POST /api/admin/billing/check` 已实现。 | +| 冻结、扣费、解冻、退费 API 存在 | 已完成 | `freeze/charge/release/refund` 已实现。 | +| 短信计费记录 API 存在 | 已完成 | `sms-billing-records` 查询和创建已实现。 | +| 阶段完整验证 | 已完成 | `npm run verify:phase5` 通过。 | + +### 阶段 5 结论 + +阶段 5 已完成。第一版可以进入阶段 6 风控与审核。 diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 0000000..9d5004d --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,44 @@ +# CMPP Gateway Spike + +阶段 0 的 `gateway/` 目录用于 Go CMPP Gateway 技术 Spike。当前环境未安装 Go 工具链,先保留工程边界和验收清单。 + +## 阶段 0 职责 + +- 读取一个 CMPP 通道配置。 +- 基于 gocmpp 或评估后的协议库完成 connect、submit、deliver、active test、terminate。 +- 消费 `cmpp.submit.commands`。 +- 发布 `cmpp.submit.results`、`cmpp.receipt.events`、`cmpp.uplink.events`。 +- 维护 `messageId -> sequenceId -> gatewayMessageId` 映射。 +- 支持断线重连和后续消息继续消费。 +- 暴露健康检查和最小指标。 + +## 建议骨架 + +```text +gateway/ +├── cmd/ +│ ├── gateway/ +│ └── smsc-simulator/ +└── internal/ + ├── cmpp/ + ├── config/ + ├── connection/ + ├── metrics/ + ├── queue/ + └── tracker/ +``` + +## 当前环境状态 + +已安装 Go 1.26.4。当前 Spike 已完成: + +- 队列消息结构定义。 +- `messageId -> sequenceId -> gatewayMessageId` 追踪器。 +- 内存模拟 submit resp 与 deliver 回执链路。 +- 15000 条内存链路压测基线。 +- gocmpp 编译级接入点。 +- gocmpp 本地 TCP connect、submit、submit resp、active test 集成测试。 +- gocmpp deliver 回执 PDU pack/unpack 测试。 +- 断线重连状态机测试。 + +阶段 0 决策:协议层优先直接依赖 gocmpp,服务层连接管理、重连、SEQID/MSGID 追踪、队列、限速、监控、幂等由本项目自研。 diff --git a/gateway/cmd/gateway/main.go b/gateway/cmd/gateway/main.go new file mode 100644 index 0000000..9654f23 --- /dev/null +++ b/gateway/cmd/gateway/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "log" + "net/http" + "os" + + "cmpp-platform/gateway/internal/health" +) + +func main() { + addr := os.Getenv("GATEWAY_HEALTH_ADDR") + if addr == "" { + addr = ":8090" + } + + log.Printf("cmpp gateway health server listening on %s", addr) + if err := http.ListenAndServe(addr, health.Handler()); err != nil { + log.Fatalf("gateway health server stopped: %v", err) + } +} diff --git a/gateway/cmd/spike/main.go b/gateway/cmd/spike/main.go new file mode 100644 index 0000000..53ebe62 --- /dev/null +++ b/gateway/cmd/spike/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "cmpp-platform/gateway/internal/spike" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + sim := spike.NewSimulator() + result, err := sim.RunLoad(ctx, 15000) + if err != nil { + log.Fatalf("run spike load: %v", err) + } + + fmt.Printf("submitted=%d submitResults=%d receiptEvents=%d duration=%s throughput=%.2f msg/s\n", + result.Submitted, + result.SubmitResults, + result.ReceiptEvents, + result.Duration, + result.MessagesPerSec, + ) +} diff --git a/gateway/go.mod b/gateway/go.mod new file mode 100644 index 0000000..fbe1c38 --- /dev/null +++ b/gateway/go.mod @@ -0,0 +1,8 @@ +module cmpp-platform/gateway + +go 1.26 + +require ( + github.com/bigwhite/gocmpp v0.0.0-20240917054108-b238366bff0b // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/gateway/go.sum b/gateway/go.sum new file mode 100644 index 0000000..0e4a8e2 --- /dev/null +++ b/gateway/go.sum @@ -0,0 +1,28 @@ +github.com/bigwhite/gocmpp v0.0.0-20240917054108-b238366bff0b h1:HOIU4bq4fpwWdtkpXASXnWk/fiFjd6o27Q0Kw4aJJAk= +github.com/bigwhite/gocmpp v0.0.0-20240917054108-b238366bff0b/go.mod h1:BDWS0X/2jJROFh0iYgdcAdv4jy3cPhVcZXvEkZmoqCM= +github.com/dvyukov/go-fuzz v0.0.0-20190516070045-5cc3605ccbb6/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gateway/internal/cmpp/client.go b/gateway/internal/cmpp/client.go new file mode 100644 index 0000000..50b2586 --- /dev/null +++ b/gateway/internal/cmpp/client.go @@ -0,0 +1,41 @@ +package cmppadapter + +import ( + "time" + + cmpp "github.com/bigwhite/gocmpp" +) + +type Version string + +const ( + Version20 Version = "2.0" + Version30 Version = "3.0" +) + +type ClientConfig struct { + Version Version + Address string + User string + Password string + Timeout time.Duration +} + +type Client struct { + raw *cmpp.Client +} + +func NewClient(config ClientConfig) *Client { + return &Client{raw: cmpp.NewClient(toProtocolType(config.Version))} +} + +func (c *Client) Raw() *cmpp.Client { + return c.raw +} + +func toProtocolType(version Version) cmpp.Type { + if version == Version20 { + return cmpp.V20 + } + return cmpp.V30 +} diff --git a/gateway/internal/cmpp/client_test.go b/gateway/internal/cmpp/client_test.go new file mode 100644 index 0000000..71e1f19 --- /dev/null +++ b/gateway/internal/cmpp/client_test.go @@ -0,0 +1,16 @@ +package cmppadapter + +import "testing" + +func TestNewClientUsesGocmpp(t *testing.T) { + client := NewClient(ClientConfig{Version: Version30}) + if client.Raw() == nil { + t.Fatal("expected gocmpp client") + } +} + +func TestProtocolVersionMapping(t *testing.T) { + if toProtocolType(Version20).String() == toProtocolType(Version30).String() { + t.Fatal("expected CMPP 2.0 and 3.0 to map to different protocol types") + } +} diff --git a/gateway/internal/cmpp/gocmpp_integration_test.go b/gateway/internal/cmpp/gocmpp_integration_test.go new file mode 100644 index 0000000..a6c7677 --- /dev/null +++ b/gateway/internal/cmpp/gocmpp_integration_test.go @@ -0,0 +1,214 @@ +package cmppadapter + +import ( + "bytes" + "crypto/md5" + "fmt" + "log" + "net" + "testing" + "time" + + cmpp "github.com/bigwhite/gocmpp" + cmpputils "github.com/bigwhite/gocmpp/utils" +) + +const ( + spikeUser = "900001" + spikePassword = "888888" +) + +func TestGocmppConnectSubmitAndActiveTest(t *testing.T) { + addr := reserveTCPAddr(t) + handlers := []cmpp.Handler{ + cmpp.HandlerFunc(handleSpikeLogin), + cmpp.HandlerFunc(handleSpikeSubmit), + } + + go func() { + err := cmpp.ListenAndServe(addr, cmpp.V30, 2*time.Second, 3, nil, handlers...) + if err != nil { + log.Printf("gocmpp spike server stopped: %v", err) + } + }() + time.Sleep(300 * time.Millisecond) + + client := cmpp.NewClient(cmpp.V30) + defer client.Disconnect() + + if err := client.Connect(addr, spikeUser, spikePassword, 2*time.Second); err != nil { + t.Fatalf("connect gocmpp server: %v", err) + } + + content, err := cmpputils.Utf8ToUcs2("测试gocmpp submit") + if err != nil { + t.Fatalf("encode submit content: %v", err) + } + + _, err = client.SendReqPkt(&cmpp.Cmpp3SubmitReqPkt{ + PkTotal: 1, + PkNumber: 1, + RegisteredDelivery: 1, + MsgLevel: 1, + ServiceId: "test", + FeeUserType: 2, + FeeTerminalId: "13500002696", + FeeTerminalType: 0, + MsgFmt: 8, + MsgSrc: spikeUser, + FeeType: "02", + FeeCode: "10", + ValidTime: "151105131555101+", + AtTime: "", + SrcId: spikeUser, + DestUsrTl: 1, + DestTerminalId: []string{"13500002696"}, + DestTerminalType: 0, + MsgLength: uint8(len(content)), + MsgContent: content, + }) + if err != nil { + t.Fatalf("send submit: %v", err) + } + + submitRsp := recvUntil[*cmpp.Cmpp3SubmitRspPkt](t, client, 2*time.Second) + if submitRsp.Result != 0 { + t.Fatalf("unexpected submit result: %d", submitRsp.Result) + } + if submitRsp.MsgId == 0 { + t.Fatal("expected gateway msg id") + } + + _, err = client.SendReqPkt(&cmpp.CmppActiveTestReqPkt{}) + if err != nil { + t.Fatalf("send active test: %v", err) + } + _ = recvUntil[*cmpp.CmppActiveTestRspPkt](t, client, 2*time.Second) +} + +func TestGocmppDeliverReceiptPackAndUnpack(t *testing.T) { + receipt := &cmpp.CmppReceiptPkt{ + MsgId: 12878564852733378560, + Stat: "DELIVRD", + SubmitTime: "2607010900", + DoneTime: "2607010901", + DestTerminalId: "13500002696", + SmscSequence: 42, + } + receiptBytes, err := receipt.Pack() + if err != nil { + t.Fatalf("pack receipt: %v", err) + } + + deliver := &cmpp.Cmpp3DeliverReqPkt{ + MsgId: 12878564852733378560, + DestId: "106900000000", + ServiceId: "test", + TpPid: 0, + TpUdhi: 0, + MsgFmt: 0, + SrcTerminalId: "13500002696", + SrcTerminalType: 0, + RegisterDelivery: 1, + MsgLength: uint8(cmpp.CmppReceiptPktLen), + MsgContent: string(receiptBytes), + } + + data, err := deliver.Pack(1001) + if err != nil { + t.Fatalf("pack deliver receipt: %v", err) + } + + var unpacked cmpp.Cmpp3DeliverReqPkt + if err := unpacked.Unpack(data[8:]); err != nil { + t.Fatalf("unpack deliver receipt: %v", err) + } + + var gotReceipt cmpp.CmppReceiptPkt + if err := gotReceipt.Unpack([]byte(unpacked.MsgContent)); err != nil { + t.Fatalf("unpack receipt content: %v", err) + } + if gotReceipt.Stat != "DELIVRD" || gotReceipt.SmscSequence != 42 { + t.Fatalf("unexpected receipt payload: %+v", &gotReceipt) + } +} + +func reserveTCPAddr(t *testing.T) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve tcp addr: %v", err) + } + addr := listener.Addr().String() + if err := listener.Close(); err != nil { + t.Fatalf("close reserved listener: %v", err) + } + return addr +} + +func recvUntil[T any](t *testing.T, client *cmpp.Client, timeout time.Duration) T { + t.Helper() + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + packet, err := client.RecvAndUnpackPkt(200 * time.Millisecond) + if err != nil { + continue + } + if typed, ok := packet.(T); ok { + return typed + } + } + + var zero T + t.Fatalf("timed out waiting for %T", zero) + return zero +} + +func handleSpikeLogin(response *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) { + req, ok := packet.Packer.(*cmpp.CmppConnReqPkt) + if !ok { + return true, nil + } + + resp := response.Packer.(*cmpp.Cmpp3ConnRspPkt) + resp.Version = 0x30 + + if req.SrcAddr != cmpputils.OctetString(spikeUser, 6) { + resp.Status = uint32(cmpp.ErrnoConnInvalidSrcAddr) + return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnInvalidSrcAddr] + } + + authSrc := md5.Sum(bytes.Join([][]byte{ + []byte(cmpputils.OctetString(spikeUser, 6)), + make([]byte, 9), + []byte(spikePassword), + []byte(cmpputils.TimeStamp2Str(req.Timestamp)), + }, nil)) + + if req.AuthSrc != string(authSrc[:]) { + resp.Status = uint32(cmpp.ErrnoConnAuthFailed) + return false, cmpp.ConnRspStatusErrMap[cmpp.ErrnoConnAuthFailed] + } + + authIsmg := md5.Sum(bytes.Join([][]byte{{byte(resp.Status)}, authSrc[:], []byte(spikePassword)}, nil)) + resp.AuthIsmg = string(authIsmg[:]) + return false, nil +} + +func handleSpikeSubmit(response *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) { + req, ok := packet.Packer.(*cmpp.Cmpp3SubmitReqPkt) + if !ok { + return true, nil + } + + if req.DestUsrTl == 0 || len(req.DestTerminalId) == 0 { + return false, fmt.Errorf("missing submit destination") + } + + resp := response.Packer.(*cmpp.Cmpp3SubmitRspPkt) + resp.MsgId = 12878564852733378560 + resp.Result = 0 + return false, nil +} diff --git a/gateway/internal/connection/reconnector.go b/gateway/internal/connection/reconnector.go new file mode 100644 index 0000000..cba6b54 --- /dev/null +++ b/gateway/internal/connection/reconnector.go @@ -0,0 +1,44 @@ +package connection + +import ( + "context" + "time" +) + +type DialFunc func(context.Context) error + +type Reconnector struct { + MaxAttempts int + Delay time.Duration + Dial DialFunc +} + +func (r Reconnector) Connect(ctx context.Context) (int, error) { + attempts := r.MaxAttempts + if attempts <= 0 { + attempts = 1 + } + + var lastErr error + for attempt := 1; attempt <= attempts; attempt++ { + if err := ctx.Err(); err != nil { + return attempt - 1, err + } + + if err := r.Dial(ctx); err != nil { + lastErr = err + if attempt < attempts && r.Delay > 0 { + select { + case <-ctx.Done(): + return attempt, ctx.Err() + case <-time.After(r.Delay): + } + } + continue + } + + return attempt, nil + } + + return attempts, lastErr +} diff --git a/gateway/internal/connection/reconnector_test.go b/gateway/internal/connection/reconnector_test.go new file mode 100644 index 0000000..bfd5940 --- /dev/null +++ b/gateway/internal/connection/reconnector_test.go @@ -0,0 +1,47 @@ +package connection + +import ( + "context" + "errors" + "testing" +) + +func TestReconnectorRetriesAfterDisconnect(t *testing.T) { + failures := 0 + reconnector := Reconnector{ + MaxAttempts: 3, + Dial: func(context.Context) error { + failures++ + if failures < 2 { + return errors.New("simulated disconnect") + } + return nil + }, + } + + attempts, err := reconnector.Connect(context.Background()) + if err != nil { + t.Fatalf("connect after retry: %v", err) + } + if attempts != 2 { + t.Fatalf("expected success on second attempt, got %d", attempts) + } +} + +func TestReconnectorReturnsLastError(t *testing.T) { + expected := errors.New("still disconnected") + reconnector := Reconnector{ + MaxAttempts: 2, + Dial: func(context.Context) error { + return expected + }, + } + + attempts, err := reconnector.Connect(context.Background()) + if !errors.Is(err, expected) { + t.Fatalf("expected last error, got %v", err) + } + if attempts != 2 { + t.Fatalf("expected two attempts, got %d", attempts) + } +} diff --git a/gateway/internal/health/server.go b/gateway/internal/health/server.go new file mode 100644 index 0000000..1d14978 --- /dev/null +++ b/gateway/internal/health/server.go @@ -0,0 +1,26 @@ +package health + +import ( + "encoding/json" + "net/http" + "time" +) + +type Status struct { + Status string `json:"status"` + Service string `json:"service"` + Timestamp string `json:"timestamp"` +} + +func Handler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(Status{ + Status: "ok", + Service: "cmpp-gateway", + Timestamp: time.Now().UTC().Format(time.RFC3339Nano), + }) + }) + return mux +} diff --git a/gateway/internal/health/server_test.go b/gateway/internal/health/server_test.go new file mode 100644 index 0000000..95ebb8e --- /dev/null +++ b/gateway/internal/health/server_test.go @@ -0,0 +1,31 @@ +package health + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthHandler(t *testing.T) { + server := httptest.NewServer(Handler()) + defer server.Close() + + response, err := http.Get(server.URL + "/health") + if err != nil { + t.Fatalf("get health: %v", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + t.Fatalf("unexpected status: %d", response.StatusCode) + } + + var payload Status + if err := json.NewDecoder(response.Body).Decode(&payload); err != nil { + t.Fatalf("decode health payload: %v", err) + } + if payload.Status != "ok" || payload.Service != "cmpp-gateway" { + t.Fatalf("unexpected payload: %+v", payload) + } +} diff --git a/gateway/internal/queue/messages.go b/gateway/internal/queue/messages.go new file mode 100644 index 0000000..7b2ca1a --- /dev/null +++ b/gateway/internal/queue/messages.go @@ -0,0 +1,90 @@ +package queue + +import "time" + +const SchemaVersion = "v1" + +type MessageType string + +const ( + MessageTypeSubmitCommand MessageType = "SubmitCommand" + MessageTypeSubmitResult MessageType = "SubmitResult" + MessageTypeReceiptEvent MessageType = "ReceiptEvent" + MessageTypeUplinkEvent MessageType = "UplinkEvent" +) + +type Envelope struct { + SchemaVersion string `json:"schemaVersion"` + MessageType MessageType `json:"messageType"` + TraceID string `json:"traceId"` + MessageID string `json:"messageId"` + ChannelID string `json:"channelId"` + CreatedAt time.Time `json:"createdAt"` +} + +type SubmitCommand struct { + Envelope + TenantID string `json:"tenantId"` + ApplicationID string `json:"applicationId"` + TaskID string `json:"taskId,omitempty"` + SubmitID string `json:"submitId"` + PhoneNumber string `json:"phoneNumber"` + Content string `json:"content"` + Signature string `json:"signature"` + TemplateID string `json:"templateId"` + BillingUnits int `json:"billingUnits"` + Route Route `json:"route"` + CMPP CMPP `json:"cmpp"` + Retry Retry `json:"retry"` +} + +type Route struct { + ChannelCode string `json:"channelCode"` + CMPPAccountCode string `json:"cmppAccountCode"` + Priority int `json:"priority"` + RateLimitPerSecond int `json:"rateLimitPerSecond,omitempty"` +} + +type CMPP struct { + ServiceID string `json:"serviceId"` + SrcID string `json:"srcId"` + RegisteredDelivery int `json:"registeredDelivery"` + MsgFmt int `json:"msgFmt"` + FeeUserType int `json:"feeUserType,omitempty"` + FeeCode string `json:"feeCode,omitempty"` + FeeType string `json:"feeType,omitempty"` +} + +type Retry struct { + Attempt int `json:"attempt"` + MaxAttempts int `json:"maxAttempts"` +} + +type SubmitResult struct { + Envelope + SequenceID uint32 `json:"sequenceId"` + GatewayMessageID string `json:"gatewayMessageId"` + SubmitStatus string `json:"submitStatus"` + ErrorCode string `json:"errorCode,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + SubmittedAt time.Time `json:"submittedAt"` +} + +type ReceiptEvent struct { + Envelope + SequenceID uint32 `json:"sequenceId"` + GatewayMessageID string `json:"gatewayMessageId"` + ReceiptStatus string `json:"receiptStatus"` + RawStatus string `json:"rawStatus"` + ErrorCode string `json:"errorCode,omitempty"` + DeliveredAt time.Time `json:"deliveredAt"` +} + +type UplinkEvent struct { + Envelope + SequenceID uint32 `json:"sequenceId"` + PhoneNumber string `json:"phoneNumber"` + DestID string `json:"destId"` + Content string `json:"content"` + ReceivedAt time.Time `json:"receivedAt"` +} diff --git a/gateway/internal/spike/simulator.go b/gateway/internal/spike/simulator.go new file mode 100644 index 0000000..73d9ea7 --- /dev/null +++ b/gateway/internal/spike/simulator.go @@ -0,0 +1,139 @@ +package spike + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "cmpp-platform/gateway/internal/queue" + "cmpp-platform/gateway/internal/tracker" +) + +type Simulator struct { + tracker *tracker.Tracker + sequence atomic.Uint32 +} + +type RunResult struct { + Submitted int + SubmitResults int + ReceiptEvents int + Duration time.Duration + MessagesPerSec float64 +} + +func NewSimulator() *Simulator { + return &Simulator{tracker: tracker.New()} +} + +func (s *Simulator) Submit(ctx context.Context, cmd queue.SubmitCommand) (queue.SubmitResult, queue.ReceiptEvent, error) { + if err := ctx.Err(); err != nil { + return queue.SubmitResult{}, queue.ReceiptEvent{}, err + } + + sequenceID := s.sequence.Add(1) + s.tracker.TrackSubmit(cmd.MessageID, sequenceID) + + gatewayMessageID := fmt.Sprintf("gw-%s", cmd.MessageID) + mapping, err := s.tracker.TrackSubmitResp(sequenceID, gatewayMessageID) + if err != nil { + return queue.SubmitResult{}, queue.ReceiptEvent{}, err + } + + now := time.Now().UTC() + result := queue.SubmitResult{ + Envelope: queue.Envelope{ + SchemaVersion: queue.SchemaVersion, + MessageType: queue.MessageTypeSubmitResult, + TraceID: cmd.TraceID, + MessageID: mapping.MessageID, + ChannelID: cmd.ChannelID, + CreatedAt: now, + }, + SequenceID: mapping.SequenceID, + GatewayMessageID: mapping.GatewayMessageID, + SubmitStatus: "accepted", + SubmittedAt: now, + } + + receipt := queue.ReceiptEvent{ + Envelope: queue.Envelope{ + SchemaVersion: queue.SchemaVersion, + MessageType: queue.MessageTypeReceiptEvent, + TraceID: cmd.TraceID, + MessageID: mapping.MessageID, + ChannelID: cmd.ChannelID, + CreatedAt: now.Add(10 * time.Millisecond), + }, + SequenceID: mapping.SequenceID, + GatewayMessageID: mapping.GatewayMessageID, + ReceiptStatus: "delivered", + RawStatus: "DELIVRD", + DeliveredAt: now.Add(10 * time.Millisecond), + } + + return result, receipt, nil +} + +func (s *Simulator) RunLoad(ctx context.Context, count int) (RunResult, error) { + start := time.Now() + result := RunResult{Submitted: count} + + for i := 0; i < count; i++ { + cmd := NewSubmitCommand(i) + _, _, err := s.Submit(ctx, cmd) + if err != nil { + return RunResult{}, err + } + result.SubmitResults++ + result.ReceiptEvents++ + } + + result.Duration = time.Since(start) + if result.Duration > 0 { + result.MessagesPerSec = float64(count) / result.Duration.Seconds() + } + + return result, nil +} + +func NewSubmitCommand(index int) queue.SubmitCommand { + now := time.Now().UTC() + messageID := fmt.Sprintf("msg-spike-%06d", index) + return queue.SubmitCommand{ + Envelope: queue.Envelope{ + SchemaVersion: queue.SchemaVersion, + MessageType: queue.MessageTypeSubmitCommand, + TraceID: fmt.Sprintf("trace-spike-%06d", index), + MessageID: messageID, + ChannelID: "sms-channel-cmpp-spike", + CreatedAt: now, + }, + TenantID: "tenant-spike", + ApplicationID: "app-spike", + TaskID: "task-spike", + SubmitID: fmt.Sprintf("submit-spike-%06d", index), + PhoneNumber: "13800138000", + Content: "您的验证码为 123456,5 分钟内有效。", + Signature: "测试平台", + TemplateID: "tpl-spike", + BillingUnits: 1, + Route: queue.Route{ + ChannelCode: "CMCC-CMPP-SPIKE", + CMPPAccountCode: "cmpp-account-spike", + Priority: 10, + RateLimitPerSecond: 500, + }, + CMPP: queue.CMPP{ + ServiceID: "CMPP", + SrcID: "106900000000", + RegisteredDelivery: 1, + MsgFmt: 15, + FeeUserType: 2, + FeeCode: "0", + FeeType: "01", + }, + Retry: queue.Retry{Attempt: 0, MaxAttempts: 3}, + } +} diff --git a/gateway/internal/spike/simulator_test.go b/gateway/internal/spike/simulator_test.go new file mode 100644 index 0000000..bf636b1 --- /dev/null +++ b/gateway/internal/spike/simulator_test.go @@ -0,0 +1,48 @@ +package spike + +import ( + "context" + "testing" + "time" +) + +func TestSimulatorRunsOneMessageLifecycle(t *testing.T) { + sim := NewSimulator() + cmd := NewSubmitCommand(1) + + result, receipt, err := sim.Submit(context.Background(), cmd) + if err != nil { + t.Fatalf("submit: %v", err) + } + + if result.MessageID != cmd.MessageID { + t.Fatalf("submit result message id mismatch: %s != %s", result.MessageID, cmd.MessageID) + } + if result.SequenceID == 0 { + t.Fatal("expected sequence id") + } + if receipt.GatewayMessageID != result.GatewayMessageID { + t.Fatalf("receipt gateway id mismatch: %s != %s", receipt.GatewayMessageID, result.GatewayMessageID) + } + if receipt.ReceiptStatus != "delivered" { + t.Fatalf("unexpected receipt status: %s", receipt.ReceiptStatus) + } +} + +func TestSimulatorLoadMeets500TPSFloor(t *testing.T) { + sim := NewSimulator() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + result, err := sim.RunLoad(ctx, 15000) + if err != nil { + t.Fatalf("run load: %v", err) + } + + if result.SubmitResults != 15000 || result.ReceiptEvents != 15000 { + t.Fatalf("unexpected result counts: %+v", result) + } + if result.MessagesPerSec < 500 { + t.Fatalf("expected at least 500 msg/s, got %.2f", result.MessagesPerSec) + } +} diff --git a/gateway/internal/tracker/tracker.go b/gateway/internal/tracker/tracker.go new file mode 100644 index 0000000..43ca82a --- /dev/null +++ b/gateway/internal/tracker/tracker.go @@ -0,0 +1,83 @@ +package tracker + +import ( + "errors" + "sync" +) + +var ErrMappingNotFound = errors.New("tracker mapping not found") + +type Mapping struct { + MessageID string + SequenceID uint32 + GatewayMessageID string +} + +type Tracker struct { + mu sync.RWMutex + byMessage map[string]Mapping + bySeq map[uint32]string + byGateway map[string]string +} + +func New() *Tracker { + return &Tracker{ + byMessage: make(map[string]Mapping), + bySeq: make(map[uint32]string), + byGateway: make(map[string]string), + } +} + +func (t *Tracker) TrackSubmit(messageID string, sequenceID uint32) Mapping { + t.mu.Lock() + defer t.mu.Unlock() + + mapping := t.byMessage[messageID] + mapping.MessageID = messageID + mapping.SequenceID = sequenceID + t.byMessage[messageID] = mapping + t.bySeq[sequenceID] = messageID + + return mapping +} + +func (t *Tracker) TrackSubmitResp(sequenceID uint32, gatewayMessageID string) (Mapping, error) { + t.mu.Lock() + defer t.mu.Unlock() + + messageID, ok := t.bySeq[sequenceID] + if !ok { + return Mapping{}, ErrMappingNotFound + } + + mapping := t.byMessage[messageID] + mapping.GatewayMessageID = gatewayMessageID + t.byMessage[messageID] = mapping + t.byGateway[gatewayMessageID] = messageID + + return mapping, nil +} + +func (t *Tracker) ByGatewayMessageID(gatewayMessageID string) (Mapping, error) { + t.mu.RLock() + defer t.mu.RUnlock() + + messageID, ok := t.byGateway[gatewayMessageID] + if !ok { + return Mapping{}, ErrMappingNotFound + } + + return t.byMessage[messageID], nil +} + +func (t *Tracker) ByMessageID(messageID string) (Mapping, error) { + t.mu.RLock() + defer t.mu.RUnlock() + + mapping, ok := t.byMessage[messageID] + if !ok { + return Mapping{}, ErrMappingNotFound + } + + return mapping, nil +} diff --git a/gateway/internal/tracker/tracker_test.go b/gateway/internal/tracker/tracker_test.go new file mode 100644 index 0000000..a08e582 --- /dev/null +++ b/gateway/internal/tracker/tracker_test.go @@ -0,0 +1,36 @@ +package tracker + +import "testing" + +func TestTrackerMapsMessageSequenceAndGatewayIDs(t *testing.T) { + tr := New() + + submit := tr.TrackSubmit("msg-1", 1001) + if submit.MessageID != "msg-1" || submit.SequenceID != 1001 { + t.Fatalf("unexpected submit mapping: %+v", submit) + } + + resp, err := tr.TrackSubmitResp(1001, "gw-1") + if err != nil { + t.Fatalf("track submit resp: %v", err) + } + if resp.GatewayMessageID != "gw-1" { + t.Fatalf("unexpected gateway message id: %+v", resp) + } + + byGateway, err := tr.ByGatewayMessageID("gw-1") + if err != nil { + t.Fatalf("lookup by gateway id: %v", err) + } + if byGateway.MessageID != "msg-1" || byGateway.SequenceID != 1001 { + t.Fatalf("unexpected gateway lookup: %+v", byGateway) + } +} + +func TestTrackerRejectsUnknownSubmitResp(t *testing.T) { + tr := New() + + if _, err := tr.TrackSubmitResp(404, "gw-missing"); err == nil { + t.Fatal("expected missing mapping error") + } +} diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..d9a0e67 --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,54 @@ +services: + postgres: + image: postgres:16-alpine + container_name: cmpp-postgres + environment: + POSTGRES_DB: cmpp_platform + POSTGRES_USER: cmpp + POSTGRES_PASSWORD: cmpp_password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U cmpp -d cmpp_platform"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: cmpp-redis + command: ["redis-server", "--appendonly", "yes"] + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + container_name: cmpp-minio + command: ["server", "/data", "--console-address", ":9001"] + environment: + MINIO_ROOT_USER: cmpp_minio + MINIO_ROOT_PASSWORD: cmpp_minio_password + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + redis_data: + minio_data: diff --git a/package-lock.json b/package-lock.json index c4d977b..2e7a294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.1.0", "dependencies": { "@vitejs/plugin-react": "^6.0.2", + "bullmq": "^5.79.2", "dayjs": "^1.11.21", "echarts": "^6.1.0", + "ioredis": "^5.11.1", "lucide-react": "^1.18.0", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -77,6 +79,90 @@ "license": "0BSD", "optional": true }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -442,6 +528,76 @@ } } }, + "node_modules/bullmq": { + "version": "5.79.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.2.tgz", + "integrity": "sha512-FebD+8XCZl/hnS1R4to24L4EAN70XSndKZO0776M36vGRk5MKVhKlNFM8/34zXLXKyYB4QaeIPFhVXSYYGTHpQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "2.0.4", + "node-abort-controller": "3.1.1", + "semver": "7.8.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, + "node_modules/bullmq/node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/bullmq/node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "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/bullmq/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/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -455,6 +611,18 @@ "url": "https://opencollective.com/express" } }, + "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/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -467,6 +635,32 @@ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, + "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", @@ -517,6 +711,28 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.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/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -778,6 +994,18 @@ "url": "https://opencollective.com/parcel" } }, + "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/lucide-react": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz", @@ -787,6 +1015,52 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "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": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "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.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -805,6 +1079,27 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -910,6 +1205,27 @@ "react-dom": ">=18" } }, + "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/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -949,6 +1265,18 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -964,6 +1292,12 @@ "node": ">=0.10.0" } }, + "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/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/package.json b/package.json index cd96667..5e2f4db 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,24 @@ "scripts": { "dev": "vite --host 0.0.0.0", "build": "tsc --noEmit && vite build", - "preview": "vite preview --host 0.0.0.0" + "build:api": "npm --prefix api run build", + "preview": "vite preview --host 0.0.0.0", + "prisma:generate": "npm --prefix api run prisma:generate", + "spike:contracts": "node tools/spike/validate-gateway-queue-contract.mjs", + "spike:gateway": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$env:Path='C:\\Program Files\\Go\\bin;'+$env:Path; Push-Location gateway; go test ./...; Pop-Location\"", + "spike:bullmq": "node api/src/spike/bullmq-link-spike.mjs", + "verify:phase1": "npm run spike:contracts && npm run spike:gateway && npm run spike:bullmq && npm run prisma:generate && npm run build:api && npm run build", + "verify:phase2": "npm run verify:phase1", + "verify:phase3": "npm run verify:phase2", + "verify:phase4": "npm run verify:phase3", + "verify:phase5": "npm run verify:phase4" }, "dependencies": { "@vitejs/plugin-react": "^6.0.2", + "bullmq": "^5.79.2", "dayjs": "^1.11.21", "echarts": "^6.1.0", + "ioredis": "^5.11.1", "lucide-react": "^1.18.0", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/src/apps/client/ClientSettingsPage.tsx b/src/apps/client/ClientSettingsPage.tsx index 3e902f4..a3744e4 100644 --- a/src/apps/client/ClientSettingsPage.tsx +++ b/src/apps/client/ClientSettingsPage.tsx @@ -1,5 +1,5 @@ import { Save } from 'lucide-react'; -import { Button, Input, Select } from '@/components/ui'; +import { Button, Input } from '@/components/ui'; export function ClientSettingsPage() { return ( @@ -17,15 +17,6 @@ export function ClientSettingsPage() { -