From edc79e23915cca753d36b2286bc85a72f2ceb9e5 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 8 Apr 2026 20:48:26 +0900 Subject: [PATCH 1/6] chore: ignore local worktrees --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 570e729..b74eb90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +.worktrees/ .DS_Store project.private.config.json *.swp From efbf113807094487615756bc0d33041badf2e36b Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 8 Apr 2026 21:02:48 +0900 Subject: [PATCH 2/6] \ feat: bootstrap backend service\ --- backend/.env.example | 1 + backend/.eslintrc.js | 12 + backend/.prettierrc | 4 + backend/README.md | 14 + backend/jest.config.js | 9 + backend/nest-cli.json | 4 + backend/package-lock.json | 8651 +++++++++++++++++ backend/package.json | 41 + backend/src/app.module.ts | 14 + .../src/common/http-response.interceptor.ts | 20 + backend/src/main.ts | 28 + backend/test/app.e2e-spec.ts | 30 + backend/test/jest-e2e.json | 9 + backend/tsconfig.json | 22 + 14 files changed, 8859 insertions(+) create mode 100644 backend/.env.example create mode 100644 backend/.eslintrc.js create mode 100644 backend/.prettierrc create mode 100644 backend/README.md create mode 100644 backend/jest.config.js create mode 100644 backend/nest-cli.json create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/src/app.module.ts create mode 100644 backend/src/common/http-response.interceptor.ts create mode 100644 backend/src/main.ts create mode 100644 backend/test/app.e2e-spec.ts create mode 100644 backend/test/jest-e2e.json create mode 100644 backend/tsconfig.json diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..c0c68b1 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1 @@ +PORT=3000 \ No newline at end of file diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js new file mode 100644 index 0000000..3839729 --- /dev/null +++ b/backend/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + root: true, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + env: { + node: true, + jest: true, + }, + extends: [], +}; \ No newline at end of file diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..76e396e --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": true +} \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..facbd44 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,14 @@ +# SW Backend + +NestJS backend workspace for the SW Mini Program MVP/P0 local-first backend. + +## Available scripts + +- `npm run start:dev` +- `npm run build` +- `npm test` +- `npm run test:e2e` + +## Environment + +Copy `.env.example` to `.env` before running the service. \ No newline at end of file diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000..d55e8dc --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testRegex: '.*\\.e2e-spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/backend/nest-cli.json b/backend/nest-cli.json new file mode 100644 index 0000000..497863f --- /dev/null +++ b/backend/nest-cli.json @@ -0,0 +1,4 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..ac3a83b --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,8651 @@ +{ + "name": "sw-backend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sw-backend", + "version": "0.1.0", + "dependencies": { + "@nestjs/common": "^10.3.10", + "@nestjs/core": "^10.3.10", + "@nestjs/platform-express": "^10.3.10", + "@prisma/client": "^5.12.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.2", + "@nestjs/testing": "^10.3.10", + "@types/jest": "^29.5.12", + "@types/node": "^20.12.7", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "prisma": "^5.12.1", + "supertest": "^7.0.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.5" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "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/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "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/@cspotcode/source-map-support/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/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.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/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/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/@tokenizer/inflate/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/@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/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "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/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "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/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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/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/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "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", + "peer": true + }, + "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", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "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/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "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/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "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/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": 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/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": 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/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "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": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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": "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/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "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": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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", + "peer": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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", + "peer": true, + "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/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": 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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/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/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "peer": true, + "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/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "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/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/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==", + "dev": true, + "license": "ISC" + }, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..2bc87d3 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,41 @@ +{ + "name": "sw-backend", + "version": "0.1.0", + "private": true, + "scripts": { + "start:dev": "nest start --watch", + "build": "nest build", + "test": "jest", + "test:e2e": "jest --config ./test/jest-e2e.json", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@nestjs/common": "^10.3.10", + "@nestjs/core": "^10.3.10", + "@nestjs/platform-express": "^10.3.10", + "@prisma/client": "^5.12.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.2", + "@nestjs/testing": "^10.3.10", + "@types/jest": "^29.5.12", + "@types/node": "^20.12.7", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "prisma": "^5.12.1", + "supertest": "^7.0.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.5" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + } +} \ No newline at end of file diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts new file mode 100644 index 0000000..2bafe9b --- /dev/null +++ b/backend/src/app.module.ts @@ -0,0 +1,14 @@ +import { Controller, Get, Module } from '@nestjs/common'; + +@Controller() +class HealthController { + @Get('/health') + health() { + return { ok: true }; + } +} + +@Module({ + controllers: [HealthController], +}) +export class AppModule {} \ No newline at end of file diff --git a/backend/src/common/http-response.interceptor.ts b/backend/src/common/http-response.interceptor.ts new file mode 100644 index 0000000..fa07913 --- /dev/null +++ b/backend/src/common/http-response.interceptor.ts @@ -0,0 +1,20 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, map } from 'rxjs'; + +@Injectable() +export class HttpResponseInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data: unknown) => ({ + code: 200, + message: 'success', + data, + })), + ); + } +} \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts new file mode 100644 index 0000000..b477adf --- /dev/null +++ b/backend/src/main.ts @@ -0,0 +1,28 @@ +import 'reflect-metadata'; +import { + INestApplication, + RequestMethod, + ValidationPipe, +} from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { HttpResponseInterceptor } from './common/http-response.interceptor'; + +export function configureApp(app: INestApplication) { + app.setGlobalPrefix('api/v1', { + exclude: [{ path: 'health', method: RequestMethod.GET }], + }); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + app.useGlobalInterceptors(new HttpResponseInterceptor()); + app.enableCors(); +} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + configureApp(app); + await app.listen(process.env.PORT || 3000); +} + +if (require.main === module) { + void bootstrap(); +} \ No newline at end of file diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..14e0096 --- /dev/null +++ b/backend/test/app.e2e-spec.ts @@ -0,0 +1,30 @@ +import { Test } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/main'; + +describe('App bootstrap (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + configureApp(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('/health returns 200', async () => { + await request(app.getHttpServer()) + .get('/health') + .expect(200) + .expect({ code: 200, message: 'success', data: { ok: true } }); + }); +}); \ No newline at end of file diff --git a/backend/test/jest-e2e.json b/backend/test/jest-e2e.json new file mode 100644 index 0000000..9a3bac5 --- /dev/null +++ b/backend/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "testEnvironment": "node" +} \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..987a689 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "rootDir": ".", + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "types": ["node", "jest"], + "ignoreDeprecations": "6.0" + }, + "include": ["src/**/*", "test/**/*"] +} \ No newline at end of file From 3f5287a9f202ee7c6726b6b71d788f888932b0f9 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 10 Apr 2026 15:12:19 +0900 Subject: [PATCH 3/6] feat: ship backend mvp and mini program integration --- .gitignore | 3 + README.md | 15 +- app.js | 3 + backend/.env.example | 4 +- backend/README.md | 15 +- backend/docker-compose.yml | 15 + .../20260410012209_mvp_baseline/migration.sql | 207 +++ backend/prisma/migrations/migration_lock.toml | 3 + backend/prisma/schema.prisma | 169 ++ backend/prisma/seed.ts | 206 +++ backend/src/app.module.ts | 31 +- backend/src/cards/cards.controller.ts | 23 + backend/src/cards/cards.module.ts | 9 + backend/src/cards/cards.service.ts | 150 ++ backend/src/cards/dto/recommend-cards.dto.ts | 22 + backend/src/cards/dto/swipe-card.dto.ts | 16 + backend/src/common/current-user.decorator.ts | 8 + backend/src/common/test-user.guard.ts | 11 + backend/src/discovery/discovery.controller.ts | 20 + backend/src/discovery/discovery.module.ts | 9 + backend/src/discovery/discovery.service.ts | 64 + backend/src/discovery/dto/get-feed.dto.ts | 15 + backend/src/discovery/dto/publish-post.dto.ts | 15 + backend/src/main.ts | 35 +- backend/src/matching/dto/trigger-check.dto.ts | 15 + backend/src/matching/matching.controller.ts | 17 + backend/src/matching/matching.module.ts | 9 + backend/src/matching/matching.service.ts | 109 ++ .../onboarding/dto/submit-onboarding.dto.ts | 24 + .../src/onboarding/onboarding.controller.ts | 17 + backend/src/onboarding/onboarding.module.ts | 9 + backend/src/onboarding/onboarding.service.ts | 88 + backend/src/prisma/prisma.module.ts | 9 + backend/src/prisma/prisma.service.ts | 5 + backend/src/profile/profile.controller.ts | 13 + backend/src/profile/profile.module.ts | 9 + backend/src/profile/profile.service.ts | 38 + backend/test/app.e2e-spec.ts | 28 +- backend/test/cards.e2e-spec.ts | 73 + backend/test/discovery.e2e-spec.ts | 47 + backend/test/matching.e2e-spec.ts | 71 + backend/test/onboarding.e2e-spec.ts | 51 + backend/test/profile.e2e-spec.ts | 40 + backend/test/test-helpers.ts | 8 + backend/tsconfig.json | 5 +- ...26-04-08-backend-p0-implementation-plan.md | 1528 +++++++++++++++++ .../2026-04-10-backend-mvp-next-steps-plan.md | 1029 +++++++++++ .../specs/2026-04-08-backend-mvp-design.md | 1494 ++++++++++++++++ pages/discovery/discovery.js | 60 +- pages/discovery/discovery.wxml | 43 +- pages/home/home.js | 136 +- pages/onboarding/onboarding.js | 66 +- pages/profile/profile.js | 30 +- pages/profile/profile.wxml | 28 +- utils/request.js | 30 + utils/session.js | 20 + 56 files changed, 6108 insertions(+), 109 deletions(-) create mode 100644 backend/docker-compose.yml create mode 100644 backend/prisma/migrations/20260410012209_mvp_baseline/migration.sql create mode 100644 backend/prisma/migrations/migration_lock.toml create mode 100644 backend/prisma/schema.prisma create mode 100644 backend/prisma/seed.ts create mode 100644 backend/src/cards/cards.controller.ts create mode 100644 backend/src/cards/cards.module.ts create mode 100644 backend/src/cards/cards.service.ts create mode 100644 backend/src/cards/dto/recommend-cards.dto.ts create mode 100644 backend/src/cards/dto/swipe-card.dto.ts create mode 100644 backend/src/common/current-user.decorator.ts create mode 100644 backend/src/common/test-user.guard.ts create mode 100644 backend/src/discovery/discovery.controller.ts create mode 100644 backend/src/discovery/discovery.module.ts create mode 100644 backend/src/discovery/discovery.service.ts create mode 100644 backend/src/discovery/dto/get-feed.dto.ts create mode 100644 backend/src/discovery/dto/publish-post.dto.ts create mode 100644 backend/src/matching/dto/trigger-check.dto.ts create mode 100644 backend/src/matching/matching.controller.ts create mode 100644 backend/src/matching/matching.module.ts create mode 100644 backend/src/matching/matching.service.ts create mode 100644 backend/src/onboarding/dto/submit-onboarding.dto.ts create mode 100644 backend/src/onboarding/onboarding.controller.ts create mode 100644 backend/src/onboarding/onboarding.module.ts create mode 100644 backend/src/onboarding/onboarding.service.ts create mode 100644 backend/src/prisma/prisma.module.ts create mode 100644 backend/src/prisma/prisma.service.ts create mode 100644 backend/src/profile/profile.controller.ts create mode 100644 backend/src/profile/profile.module.ts create mode 100644 backend/src/profile/profile.service.ts create mode 100644 backend/test/cards.e2e-spec.ts create mode 100644 backend/test/discovery.e2e-spec.ts create mode 100644 backend/test/matching.e2e-spec.ts create mode 100644 backend/test/onboarding.e2e-spec.ts create mode 100644 backend/test/profile.e2e-spec.ts create mode 100644 backend/test/test-helpers.ts create mode 100644 docs/superpowers/plans/2026-04-08-backend-p0-implementation-plan.md create mode 100644 docs/superpowers/plans/2026-04-10-backend-mvp-next-steps-plan.md create mode 100644 docs/superpowers/specs/2026-04-08-backend-mvp-design.md create mode 100644 utils/request.js create mode 100644 utils/session.js diff --git a/.gitignore b/.gitignore index b74eb90..e34f71c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules/ .DS_Store project.private.config.json *.swp +backend/.env +backend/.tmp-start*.log +backend/dist/ diff --git a/README.md b/README.md index 0a64b6c..4f07a0a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,20 @@ 2. 在微信开发者工具中,选择 **导入项目**。 3. 选择当前项目目录 (`SW`)。 4. AppID 可以填入自己的测试号或是选择“测试号”。 -5. 即可在开发者工具中进行预览和调试。 +5. 若需联调真实后端,先启动 `.worktrees/backend-p0/backend` 中的 NestJS 服务。 +6. 即可在开发者工具中进行预览和调试。 + +## Backend MVP 联调 + +- 后端目录:`.worktrees/backend-p0/backend` +- API 地址:`http://127.0.0.1:3005` +- PostgreSQL:`localhost:5434` +- 启动顺序: + 1. `docker compose up -d` + 2. `npm run prisma:generate` + 3. `npm run prisma:migrate -- --name mvp_baseline` + 4. `npm run prisma:seed` + 5. `npm run start:dev` ## 🛠 技术栈 diff --git a/app.js b/app.js index 623b8c6..2e70c50 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,6 @@ App({ + globalData: { + apiBaseUrl: 'http://127.0.0.1:3005' + }, onLaunch() {} }) diff --git a/backend/.env.example b/backend/.env.example index c0c68b1..26c77ef 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1 +1,3 @@ -PORT=3000 \ No newline at end of file +PORT=3005 +DATABASE_URL=postgresql://sw:sw@localhost:5434/sw?schema=public +TEST_USER_ID=1 diff --git a/backend/README.md b/backend/README.md index facbd44..ed7d4d7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,6 +2,19 @@ NestJS backend workspace for the SW Mini Program MVP/P0 local-first backend. +## Local run + +1. `docker compose up -d` +2. Copy `.env.example` to `.env` if needed +3. `npm install` +4. `npm run prisma:generate` +5. `npm run prisma:migrate -- --name mvp_baseline` +6. `npm run prisma:seed` +7. `npm run start:dev` + +The backend listens on `http://127.0.0.1:3005`. +PostgreSQL is exposed on `localhost:5434`. + ## Available scripts - `npm run start:dev` @@ -11,4 +24,4 @@ NestJS backend workspace for the SW Mini Program MVP/P0 local-first backend. ## Environment -Copy `.env.example` to `.env` before running the service. \ No newline at end of file +Copy `.env.example` to `.env` before running the service. diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..818e1c8 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,15 @@ +services: + postgres: + image: postgres:16 + container_name: sw-postgres + ports: + - '5434:5432' + environment: + POSTGRES_USER: sw + POSTGRES_PASSWORD: sw + POSTGRES_DB: sw + volumes: + - sw_postgres_data:/var/lib/postgresql/data + +volumes: + sw_postgres_data: diff --git a/backend/prisma/migrations/20260410012209_mvp_baseline/migration.sql b/backend/prisma/migrations/20260410012209_mvp_baseline/migration.sql new file mode 100644 index 0000000..03701f8 --- /dev/null +++ b/backend/prisma/migrations/20260410012209_mvp_baseline/migration.sql @@ -0,0 +1,207 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" BIGSERIAL NOT NULL, + "nickname" TEXT NOT NULL, + "avatarUrl" TEXT, + "gender" TEXT, + "ageRange" TEXT, + "relationshipStatus" TEXT, + "mbti" TEXT, + "signature" TEXT, + "city" TEXT, + "onboardingCompleted" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OnboardingAnswer" ( + "id" BIGSERIAL NOT NULL, + "userId" BIGINT NOT NULL, + "questionId" INTEGER NOT NULL, + "selectedValue" TEXT, + "isSkipped" BOOLEAN NOT NULL DEFAULT false, + "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "OnboardingAnswer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserProfileTag" ( + "id" BIGSERIAL NOT NULL, + "userId" BIGINT NOT NULL, + "tagType" TEXT NOT NULL, + "tagKey" TEXT NOT NULL, + "tagValue" TEXT NOT NULL, + "weight" DECIMAL(65,30) NOT NULL DEFAULT 1, + "source" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserProfileTag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Card" ( + "id" BIGSERIAL NOT NULL, + "authorUserId" BIGINT, + "sourceType" TEXT NOT NULL, + "category" TEXT NOT NULL, + "content" TEXT NOT NULL, + "status" TEXT NOT NULL, + "agreeCount" INTEGER NOT NULL DEFAULT 0, + "disagreeCount" INTEGER NOT NULL DEFAULT 0, + "skipCount" INTEGER NOT NULL DEFAULT 0, + "exposureCount" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Card_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CardExposure" ( + "id" BIGSERIAL NOT NULL, + "userId" BIGINT NOT NULL, + "cardId" BIGINT NOT NULL, + "sessionId" TEXT NOT NULL, + "positionIndex" INTEGER, + "exposedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CardExposure_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CardSwipe" ( + "id" BIGSERIAL NOT NULL, + "userId" BIGINT NOT NULL, + "cardId" BIGINT NOT NULL, + "sessionId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "sourceTab" TEXT, + "swipedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CardSwipe_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserSession" ( + "id" BIGSERIAL NOT NULL, + "sessionId" TEXT NOT NULL, + "userId" BIGINT NOT NULL, + "entryPage" TEXT NOT NULL, + "enteredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastActiveAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "validSwipeCount" INTEGER NOT NULL DEFAULT 0, + "skipCount" INTEGER NOT NULL DEFAULT 0, + "blindBoxChecked" BOOLEAN NOT NULL DEFAULT false, + "blindBoxTriggered" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MatchEvent" ( + "id" BIGSERIAL NOT NULL, + "userId" BIGINT NOT NULL, + "candidateUserId" BIGINT, + "sessionId" TEXT NOT NULL, + "triggerReason" TEXT NOT NULL, + "matchScore" DECIMAL(65,30), + "resultStatus" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MatchEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DiscoveryPost" ( + "id" BIGSERIAL NOT NULL, + "authorUserId" BIGINT NOT NULL, + "postType" TEXT NOT NULL, + "category" TEXT NOT NULL, + "title" TEXT, + "content" TEXT NOT NULL, + "anonymous" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL, + "likeCount" INTEGER NOT NULL DEFAULT 0, + "commentCount" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DiscoveryPost_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserPhoto" ( + "id" BIGSERIAL NOT NULL, + "userId" BIGINT NOT NULL, + "photoUrl" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserPhoto_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "OnboardingAnswer_userId_questionId_key" ON "OnboardingAnswer"("userId", "questionId"); + +-- CreateIndex +CREATE INDEX "CardExposure_userId_exposedAt_idx" ON "CardExposure"("userId", "exposedAt"); + +-- CreateIndex +CREATE INDEX "CardExposure_sessionId_idx" ON "CardExposure"("sessionId"); + +-- CreateIndex +CREATE INDEX "CardExposure_cardId_exposedAt_idx" ON "CardExposure"("cardId", "exposedAt"); + +-- CreateIndex +CREATE INDEX "CardSwipe_userId_swipedAt_idx" ON "CardSwipe"("userId", "swipedAt"); + +-- CreateIndex +CREATE INDEX "CardSwipe_cardId_swipedAt_idx" ON "CardSwipe"("cardId", "swipedAt"); + +-- CreateIndex +CREATE INDEX "CardSwipe_sessionId_idx" ON "CardSwipe"("sessionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CardSwipe_userId_cardId_key" ON "CardSwipe"("userId", "cardId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSession_sessionId_key" ON "UserSession"("sessionId"); + +-- AddForeignKey +ALTER TABLE "OnboardingAnswer" ADD CONSTRAINT "OnboardingAnswer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserProfileTag" ADD CONSTRAINT "UserProfileTag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Card" ADD CONSTRAINT "Card_authorUserId_fkey" FOREIGN KEY ("authorUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CardExposure" ADD CONSTRAINT "CardExposure_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "Card"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CardSwipe" ADD CONSTRAINT "CardSwipe_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CardSwipe" ADD CONSTRAINT "CardSwipe_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "Card"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MatchEvent" ADD CONSTRAINT "MatchEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MatchEvent" ADD CONSTRAINT "MatchEvent_candidateUserId_fkey" FOREIGN KEY ("candidateUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DiscoveryPost" ADD CONSTRAINT "DiscoveryPost_authorUserId_fkey" FOREIGN KEY ("authorUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserPhoto" ADD CONSTRAINT "UserPhoto_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..f7493f0 --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,169 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id BigInt @id @default(autoincrement()) + nickname String + avatarUrl String? + gender String? + ageRange String? + relationshipStatus String? + mbti String? + signature String? + city String? + onboardingCompleted Boolean @default(false) + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + onboardingAnswers OnboardingAnswer[] + profileTags UserProfileTag[] + authoredCards Card[] @relation("CardAuthor") + cardSwipes CardSwipe[] + sessions UserSession[] + matchEvents MatchEvent[] @relation("MatchRequester") + candidateMatchEvents MatchEvent[] @relation("MatchCandidate") + discoveryPosts DiscoveryPost[] + photos UserPhoto[] +} + +model OnboardingAnswer { + id BigInt @id @default(autoincrement()) + userId BigInt + questionId Int + selectedValue String? + isSkipped Boolean @default(false) + submittedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + + @@unique([userId, questionId]) +} + +model UserProfileTag { + id BigInt @id @default(autoincrement()) + userId BigInt + tagType String + tagKey String + tagValue String + weight Decimal @default(1) + source String + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id]) +} + +model Card { + id BigInt @id @default(autoincrement()) + authorUserId BigInt? + sourceType String + category String + content String + status String + agreeCount Int @default(0) + disagreeCount Int @default(0) + skipCount Int @default(0) + exposureCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User? @relation("CardAuthor", fields: [authorUserId], references: [id]) + exposures CardExposure[] + swipes CardSwipe[] +} + +model CardExposure { + id BigInt @id @default(autoincrement()) + userId BigInt + cardId BigInt + sessionId String + positionIndex Int? + exposedAt DateTime @default(now()) + + card Card @relation(fields: [cardId], references: [id]) + + @@index([userId, exposedAt]) + @@index([sessionId]) + @@index([cardId, exposedAt]) +} + +model CardSwipe { + id BigInt @id @default(autoincrement()) + userId BigInt + cardId BigInt + sessionId String + action String + sourceTab String? + swipedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + card Card @relation(fields: [cardId], references: [id]) + + @@index([userId, swipedAt]) + @@index([cardId, swipedAt]) + @@index([sessionId]) + @@unique([userId, cardId]) +} + +model UserSession { + id BigInt @id @default(autoincrement()) + sessionId String @unique + userId BigInt + entryPage String + enteredAt DateTime @default(now()) + lastActiveAt DateTime @default(now()) + validSwipeCount Int @default(0) + skipCount Int @default(0) + blindBoxChecked Boolean @default(false) + blindBoxTriggered Boolean @default(false) + + user User @relation(fields: [userId], references: [id]) +} + +model MatchEvent { + id BigInt @id @default(autoincrement()) + userId BigInt + candidateUserId BigInt? + sessionId String + triggerReason String + matchScore Decimal? + resultStatus String + createdAt DateTime @default(now()) + + user User @relation("MatchRequester", fields: [userId], references: [id]) + candidate User? @relation("MatchCandidate", fields: [candidateUserId], references: [id]) +} + +model DiscoveryPost { + id BigInt @id @default(autoincrement()) + authorUserId BigInt + postType String + category String + title String? + content String + anonymous Boolean @default(false) + status String + likeCount Int @default(0) + commentCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User @relation(fields: [authorUserId], references: [id]) +} + +model UserPhoto { + id BigInt @id @default(autoincrement()) + userId BigInt + photoUrl String + sortOrder Int @default(0) + status String @default("active") + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) +} diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..1852a3e --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,206 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { PrismaClient } from '@prisma/client'; + +const envPath = path.resolve(__dirname, '../.env'); +if (fs.existsSync(envPath)) { + const envText = fs.readFileSync(envPath, 'utf8'); + for (const line of envText.split(/\r?\n/)) { + if (!line || line.trim().startsWith('#')) { + continue; + } + + const separatorIndex = line.indexOf('='); + if (separatorIndex === -1) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + if (!(key in process.env)) { + process.env[key] = value; + } + } +} + +const prisma = new PrismaClient(); + +async function main() { + await prisma.userPhoto.deleteMany(); + await prisma.discoveryPost.deleteMany(); + await prisma.matchEvent.deleteMany(); + await prisma.cardSwipe.deleteMany(); + await prisma.userSession.deleteMany(); + await prisma.cardExposure.deleteMany(); + await prisma.card.deleteMany(); + await prisma.userProfileTag.deleteMany(); + await prisma.onboardingAnswer.deleteMany(); + await prisma.user.deleteMany(); + + await prisma.user.createMany({ + data: [ + { + id: 1n, + nickname: 'SOFIA', + avatarUrl: 'https://example.com/avatars/sofia.jpg', + gender: 'female', + ageRange: 'gen-z', + relationshipStatus: 'single', + mbti: 'INFJ', + signature: '先理解,再表达。', + city: 'Shanghai', + onboardingCompleted: true, + }, + { + id: 2n, + nickname: 'MARCUS', + avatarUrl: 'https://example.com/avatars/marcus.jpg', + gender: 'male', + ageRange: '90s', + relationshipStatus: 'single', + mbti: 'INTJ', + signature: '夜晚效率更高。', + city: 'Beijing', + onboardingCompleted: true, + }, + { + id: 3n, + nickname: 'ELENA', + avatarUrl: 'https://example.com/avatars/elena.jpg', + gender: 'female', + ageRange: '90s', + relationshipStatus: 'single', + mbti: 'ENFJ', + signature: '相信温柔也有力量。', + city: 'Hangzhou', + onboardingCompleted: true, + }, + { + id: 4n, + nickname: 'JULI', + avatarUrl: 'https://example.com/avatars/juli.jpg', + gender: 'female', + ageRange: 'gen-z', + relationshipStatus: 'single', + mbti: 'ENFP', + signature: '喜欢旅行和探索。', + city: 'Chengdu', + onboardingCompleted: true, + }, + ], + skipDuplicates: true, + }); + + await prisma.userProfileTag.createMany({ + data: [ + { userId: 1n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '价值观', source: 'onboarding' }, + { userId: 1n, tagType: 'viewpoint_style', tagKey: 'style', tagValue: 'empathetic', source: 'behavior' }, + { userId: 2n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '内心世界', source: 'behavior' }, + { userId: 2n, tagType: 'viewpoint_style', tagKey: 'style', tagValue: 'rational', source: 'behavior' }, + { userId: 3n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '价值观', source: 'behavior' }, + { userId: 3n, tagType: 'viewpoint_style', tagKey: 'style', tagValue: 'warm', source: 'behavior' }, + { userId: 4n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '旅行与探索', source: 'onboarding' }, + { userId: 4n, tagType: 'viewpoint_style', tagKey: 'style', tagValue: 'curious', source: 'behavior' }, + ], + skipDuplicates: true, + }); + + await prisma.card.createMany({ + data: [ + { + id: 101n, + authorUserId: 1n, + sourceType: 'platform', + category: '社会观察', + content: '坚定地认为《虎胆龙威》是一部圣诞电影。', + status: 'active', + agreeCount: 65, + disagreeCount: 35, + }, + { + id: 202n, + authorUserId: 2n, + sourceType: 'platform', + category: '内心世界', + content: '认为晚上的效率永远比白天高。', + status: 'active', + agreeCount: 42, + disagreeCount: 58, + }, + { + id: 303n, + authorUserId: 3n, + sourceType: 'platform', + category: '价值观', + content: '真正成熟的人会先理解,再表达自己。', + status: 'active', + agreeCount: 71, + disagreeCount: 29, + }, + { + id: 404n, + authorUserId: 4n, + sourceType: 'platform', + category: '旅行与探索', + content: '旅行的意义是重新看见熟悉的自己。', + status: 'active', + agreeCount: 54, + disagreeCount: 46, + }, + { + id: 505n, + authorUserId: 1n, + sourceType: 'platform', + category: '内心世界', + content: '树洞比热闹更能让人放松。', + status: 'active', + agreeCount: 60, + disagreeCount: 40, + }, + ], + skipDuplicates: true, + }); + + await prisma.discoveryPost.createMany({ + data: [ + { + id: 501n, + authorUserId: 1n, + postType: 'featured', + category: '价值观', + title: '关于成熟', + content: '我更认同先理解后表达。', + status: 'published', + }, + { + id: 502n, + authorUserId: 2n, + postType: 'timeline', + category: '内心世界', + content: '今晚也想把话说给风听。', + status: 'published', + }, + { + id: 503n, + authorUserId: 4n, + postType: 'timeline', + category: '旅行与探索', + content: '想去海边待三天不看手机。', + status: 'published', + }, + ], + skipDuplicates: true, + }); + + await prisma.userPhoto.createMany({ + data: [ + { userId: 1n, photoUrl: 'https://example.com/photos/sofia-1.jpg', sortOrder: 0 }, + { userId: 1n, photoUrl: 'https://example.com/photos/sofia-2.jpg', sortOrder: 1 }, + { userId: 2n, photoUrl: 'https://example.com/photos/marcus-1.jpg', sortOrder: 0 }, + { userId: 3n, photoUrl: 'https://example.com/photos/elena-1.jpg', sortOrder: 0 }, + ], + skipDuplicates: true, + }); +} + +main().finally(() => prisma.$disconnect()); diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 2bafe9b..c285866 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,14 +1,33 @@ -import { Controller, Get, Module } from '@nestjs/common'; +import { Controller, Get, Module, UseGuards } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; +import { CardsModule } from './cards/cards.module'; +import { CurrentUser } from './common/current-user.decorator'; +import { TestUserGuard } from './common/test-user.guard'; +import { DiscoveryModule } from './discovery/discovery.module'; +import { MatchingModule } from './matching/matching.module'; +import { OnboardingModule } from './onboarding/onboarding.module'; +import { ProfileModule } from './profile/profile.module'; +import { PrismaModule } from './prisma/prisma.module'; -@Controller() +@Controller('health') +@UseGuards(TestUserGuard) class HealthController { - @Get('/health') - health() { - return { ok: true }; + @Get() + health(@CurrentUser() user: { id: bigint }) { + return { ok: true, userId: user.id.toString() }; } } @Module({ + imports: [ + PrismaModule, + OnboardingModule, + CardsModule, + MatchingModule, + DiscoveryModule, + ProfileModule, + ], controllers: [HealthController], + providers: [{ provide: APP_GUARD, useClass: TestUserGuard }], }) -export class AppModule {} \ No newline at end of file +export class AppModule {} diff --git a/backend/src/cards/cards.controller.ts b/backend/src/cards/cards.controller.ts new file mode 100644 index 0000000..f615171 --- /dev/null +++ b/backend/src/cards/cards.controller.ts @@ -0,0 +1,23 @@ +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { CurrentUser } from '../common/current-user.decorator'; +import { RecommendCardsDto } from './dto/recommend-cards.dto'; +import { SwipeCardDto } from './dto/swipe-card.dto'; +import { CardsService } from './cards.service'; + +@Controller('cards') +export class CardsController { + constructor(private readonly cardsService: CardsService) {} + + @Get('recommend') + recommend( + @CurrentUser() user: { id: bigint }, + @Query() dto: RecommendCardsDto, + ) { + return this.cardsService.recommend(user.id, dto); + } + + @Post('swipe') + swipe(@CurrentUser() user: { id: bigint }, @Body() dto: SwipeCardDto) { + return this.cardsService.swipe(user.id, dto); + } +} diff --git a/backend/src/cards/cards.module.ts b/backend/src/cards/cards.module.ts new file mode 100644 index 0000000..f32c8f1 --- /dev/null +++ b/backend/src/cards/cards.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CardsController } from './cards.controller'; +import { CardsService } from './cards.service'; + +@Module({ + controllers: [CardsController], + providers: [CardsService], +}) +export class CardsModule {} diff --git a/backend/src/cards/cards.service.ts b/backend/src/cards/cards.service.ts new file mode 100644 index 0000000..4096d0d --- /dev/null +++ b/backend/src/cards/cards.service.ts @@ -0,0 +1,150 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { RecommendCardsDto } from './dto/recommend-cards.dto'; +import { SwipeCardDto } from './dto/swipe-card.dto'; + +@Injectable() +export class CardsService { + constructor(private readonly prisma: PrismaService) {} + + async recommend(userId: bigint, dto: RecommendCardsDto) { + const cards = await this.prisma.card.findMany({ + where: { + status: 'active', + category: dto.category && dto.category !== '全部' ? dto.category : undefined, + swipes: { + none: { + userId, + }, + }, + }, + include: { + author: true, + }, + orderBy: [{ agreeCount: 'desc' }, { createdAt: 'desc' }], + take: dto.limit, + }); + + if (cards.length > 0) { + await this.prisma.cardExposure.createMany({ + data: cards.map((card, index) => ({ + userId, + cardId: card.id, + sessionId: dto.sessionId ?? 'default-session', + positionIndex: index, + })), + }); + } + + const avatarPool = await this.prisma.user.findMany({ + where: { avatarUrl: { not: null } }, + orderBy: { id: 'asc' }, + take: 3, + }); + + return { + cursor: null, + items: cards.map((card) => ({ + cardId: card.id.toString(), + content: card.content, + tags: card.category, + user: { + userId: card.author?.id?.toString() ?? '0', + name: card.author?.nickname ?? 'SW', + avatar: card.author?.avatarUrl ?? '', + }, + stats: { + agreePercent: + card.agreeCount + card.disagreeCount === 0 + ? 0 + : Math.round((card.agreeCount / (card.agreeCount + card.disagreeCount)) * 100), + agreeAvatars: avatarPool + .slice(0, 2) + .map((user) => user.avatarUrl) + .filter((value): value is string => Boolean(value)), + disagreeAvatar: avatarPool[2]?.avatarUrl ?? avatarPool[0]?.avatarUrl ?? '', + }, + })), + }; + } + + async swipe(userId: bigint, dto: SwipeCardDto) { + const cardId = BigInt(dto.cardId); + + const session = await this.prisma.$transaction(async (tx) => { + const existingSwipe = await tx.cardSwipe.findUnique({ + where: { + userId_cardId: { + userId, + cardId, + }, + }, + }); + + await tx.cardSwipe.upsert({ + where: { + userId_cardId: { + userId, + cardId, + }, + }, + create: { + userId, + cardId, + action: dto.action, + sessionId: dto.sessionId, + sourceTab: dto.sourceTab, + }, + update: { + action: dto.action, + sessionId: dto.sessionId, + sourceTab: dto.sourceTab, + swipedAt: new Date(), + }, + }); + + if (!existingSwipe) { + await tx.card.update({ + where: { id: cardId }, + data: + dto.action === 'agree' + ? { agreeCount: { increment: 1 } } + : dto.action === 'disagree' + ? { disagreeCount: { increment: 1 } } + : { skipCount: { increment: 1 } }, + }); + } + + return tx.userSession.upsert({ + where: { sessionId: dto.sessionId }, + create: { + sessionId: dto.sessionId, + userId, + entryPage: 'home', + validSwipeCount: dto.action === 'skip' ? 0 : 1, + skipCount: dto.action === 'skip' ? 1 : 0, + }, + update: { + lastActiveAt: new Date(), + validSwipeCount: + dto.action === 'skip' + ? undefined + : { + increment: 1, + }, + skipCount: + dto.action === 'skip' + ? { + increment: 1, + } + : undefined, + }, + }); + }); + + return { + recorded: true, + sessionSwipeCount: session.validSwipeCount + session.skipCount, + }; + } +} diff --git a/backend/src/cards/dto/recommend-cards.dto.ts b/backend/src/cards/dto/recommend-cards.dto.ts new file mode 100644 index 0000000..3f0d9b7 --- /dev/null +++ b/backend/src/cards/dto/recommend-cards.dto.ts @@ -0,0 +1,22 @@ +import { Type } from 'class-transformer'; +import { IsOptional, IsString, Max, Min } from 'class-validator'; + +export class RecommendCardsDto { + @IsOptional() + @Type(() => Number) + @Min(1) + @Max(20) + limit = 1; + + @IsOptional() + @IsString() + cursor?: string; + + @IsOptional() + @IsString() + category?: string; + + @IsOptional() + @IsString() + sessionId?: string; +} diff --git a/backend/src/cards/dto/swipe-card.dto.ts b/backend/src/cards/dto/swipe-card.dto.ts new file mode 100644 index 0000000..0bf6f94 --- /dev/null +++ b/backend/src/cards/dto/swipe-card.dto.ts @@ -0,0 +1,16 @@ +import { IsIn, IsOptional, IsString } from 'class-validator'; + +export class SwipeCardDto { + @IsString() + cardId!: string; + + @IsIn(['agree', 'disagree', 'skip']) + action!: 'agree' | 'disagree' | 'skip'; + + @IsString() + sessionId!: string; + + @IsOptional() + @IsString() + sourceTab?: string; +} diff --git a/backend/src/common/current-user.decorator.ts b/backend/src/common/current-user.decorator.ts new file mode 100644 index 0000000..92caf4c --- /dev/null +++ b/backend/src/common/current-user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const CurrentUser = createParamDecorator( + (_data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.currentUser as { id: bigint }; + }, +); diff --git a/backend/src/common/test-user.guard.ts b/backend/src/common/test-user.guard.ts new file mode 100644 index 0000000..f1f25b8 --- /dev/null +++ b/backend/src/common/test-user.guard.ts @@ -0,0 +1,11 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +@Injectable() +export class TestUserGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const rawUserId = request.headers['x-test-user-id'] ?? process.env.TEST_USER_ID ?? '1'; + request.currentUser = { id: BigInt(Array.isArray(rawUserId) ? rawUserId[0] : rawUserId) }; + return true; + } +} diff --git a/backend/src/discovery/discovery.controller.ts b/backend/src/discovery/discovery.controller.ts new file mode 100644 index 0000000..3c19b0f --- /dev/null +++ b/backend/src/discovery/discovery.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { CurrentUser } from '../common/current-user.decorator'; +import { DiscoveryService } from './discovery.service'; +import { GetFeedDto } from './dto/get-feed.dto'; +import { PublishPostDto } from './dto/publish-post.dto'; + +@Controller('discovery') +export class DiscoveryController { + constructor(private readonly discoveryService: DiscoveryService) {} + + @Get('feed') + getFeed(@Query() dto: GetFeedDto) { + return this.discoveryService.getFeed(dto); + } + + @Post('publish') + publish(@CurrentUser() user: { id: bigint }, @Body() dto: PublishPostDto) { + return this.discoveryService.publish(user.id, dto); + } +} diff --git a/backend/src/discovery/discovery.module.ts b/backend/src/discovery/discovery.module.ts new file mode 100644 index 0000000..f5369a8 --- /dev/null +++ b/backend/src/discovery/discovery.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DiscoveryController } from './discovery.controller'; +import { DiscoveryService } from './discovery.service'; + +@Module({ + controllers: [DiscoveryController], + providers: [DiscoveryService], +}) +export class DiscoveryModule {} diff --git a/backend/src/discovery/discovery.service.ts b/backend/src/discovery/discovery.service.ts new file mode 100644 index 0000000..a8f3cd3 --- /dev/null +++ b/backend/src/discovery/discovery.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { GetFeedDto } from './dto/get-feed.dto'; +import { PublishPostDto } from './dto/publish-post.dto'; + +@Injectable() +export class DiscoveryService { + constructor(private readonly prisma: PrismaService) {} + + async getFeed(dto: GetFeedDto) { + const items = await this.prisma.discoveryPost.findMany({ + where: { + status: 'published', + category: dto.tabType && dto.tabType !== '全部' ? dto.tabType : undefined, + postType: dto.feedType, + }, + include: { + author: true, + }, + orderBy: [{ createdAt: 'desc' }], + take: 10, + }); + + return { + cursor: null, + items: items.map((item) => ({ + feedId: item.id.toString(), + type: item.postType, + category: item.category, + title: item.title, + content: item.content, + anonymous: item.anonymous, + author: { + userId: item.author.id.toString(), + nickname: item.author.nickname, + avatar: item.author.avatarUrl ?? '', + }, + stats: { + likeCount: item.likeCount, + commentCount: item.commentCount, + }, + createdAt: item.createdAt.toISOString(), + })), + }; + } + + async publish(userId: bigint, dto: PublishPostDto) { + const created = await this.prisma.discoveryPost.create({ + data: { + authorUserId: userId, + postType: 'timeline', + category: dto.tabType, + content: dto.content, + anonymous: dto.anonymous, + status: 'published', + }, + }); + + return { + feedId: created.id.toString(), + status: created.status, + }; + } +} diff --git a/backend/src/discovery/dto/get-feed.dto.ts b/backend/src/discovery/dto/get-feed.dto.ts new file mode 100644 index 0000000..388a3f5 --- /dev/null +++ b/backend/src/discovery/dto/get-feed.dto.ts @@ -0,0 +1,15 @@ +import { IsIn, IsOptional, IsString } from 'class-validator'; + +export class GetFeedDto { + @IsOptional() + @IsString() + tabType?: string; + + @IsOptional() + @IsIn(['featured', 'timeline']) + feedType?: 'featured' | 'timeline'; + + @IsOptional() + @IsString() + cursor?: string; +} diff --git a/backend/src/discovery/dto/publish-post.dto.ts b/backend/src/discovery/dto/publish-post.dto.ts new file mode 100644 index 0000000..f67d5f5 --- /dev/null +++ b/backend/src/discovery/dto/publish-post.dto.ts @@ -0,0 +1,15 @@ +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; + +export class PublishPostDto { + @IsString() + content!: string; + + @IsString() + tabType!: string; + + @IsOptional() + @Transform(({ value }) => value === true || value === 'true') + @IsBoolean() + anonymous = false; +} diff --git a/backend/src/main.ts b/backend/src/main.ts index b477adf..9c2897b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,17 +1,34 @@ import 'reflect-metadata'; -import { - INestApplication, - RequestMethod, - ValidationPipe, -} from '@nestjs/common'; +import fs from 'node:fs'; +import path from 'node:path'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpResponseInterceptor } from './common/http-response.interceptor'; +const envPath = path.resolve(process.cwd(), '.env'); +if (fs.existsSync(envPath)) { + const envText = fs.readFileSync(envPath, 'utf8'); + for (const line of envText.split(/\r?\n/)) { + if (!line || line.trim().startsWith('#')) { + continue; + } + + const separatorIndex = line.indexOf('='); + if (separatorIndex === -1) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + if (!(key in process.env)) { + process.env[key] = value; + } + } +} + export function configureApp(app: INestApplication) { - app.setGlobalPrefix('api/v1', { - exclude: [{ path: 'health', method: RequestMethod.GET }], - }); + app.setGlobalPrefix('api/v1'); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.useGlobalInterceptors(new HttpResponseInterceptor()); app.enableCors(); @@ -25,4 +42,4 @@ async function bootstrap() { if (require.main === module) { void bootstrap(); -} \ No newline at end of file +} diff --git a/backend/src/matching/dto/trigger-check.dto.ts b/backend/src/matching/dto/trigger-check.dto.ts new file mode 100644 index 0000000..06db88d --- /dev/null +++ b/backend/src/matching/dto/trigger-check.dto.ts @@ -0,0 +1,15 @@ +import { Type } from 'class-transformer'; +import { IsString, Min } from 'class-validator'; + +export class TriggerCheckDto { + @IsString() + sessionId!: string; + + @Type(() => Number) + @Min(0) + sessionSwipeCount!: number; + + @Type(() => Number) + @Min(0) + sessionDuration!: number; +} diff --git a/backend/src/matching/matching.controller.ts b/backend/src/matching/matching.controller.ts new file mode 100644 index 0000000..e328d79 --- /dev/null +++ b/backend/src/matching/matching.controller.ts @@ -0,0 +1,17 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { CurrentUser } from '../common/current-user.decorator'; +import { TriggerCheckDto } from './dto/trigger-check.dto'; +import { MatchingService } from './matching.service'; + +@Controller('matching') +export class MatchingController { + constructor(private readonly matchingService: MatchingService) {} + + @Post('trigger-check') + triggerCheck( + @CurrentUser() user: { id: bigint }, + @Body() dto: TriggerCheckDto, + ) { + return this.matchingService.triggerCheck(user.id, dto); + } +} diff --git a/backend/src/matching/matching.module.ts b/backend/src/matching/matching.module.ts new file mode 100644 index 0000000..3c41f90 --- /dev/null +++ b/backend/src/matching/matching.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { MatchingController } from './matching.controller'; +import { MatchingService } from './matching.service'; + +@Module({ + controllers: [MatchingController], + providers: [MatchingService], +}) +export class MatchingModule {} diff --git a/backend/src/matching/matching.service.ts b/backend/src/matching/matching.service.ts new file mode 100644 index 0000000..e43a77e --- /dev/null +++ b/backend/src/matching/matching.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { TriggerCheckDto } from './dto/trigger-check.dto'; + +@Injectable() +export class MatchingService { + constructor(private readonly prisma: PrismaService) {} + + async triggerCheck(userId: bigint, dto: TriggerCheckDto) { + const session = await this.prisma.userSession.upsert({ + where: { sessionId: dto.sessionId }, + create: { + sessionId: dto.sessionId, + userId, + entryPage: 'home', + validSwipeCount: dto.sessionSwipeCount, + }, + update: { + lastActiveAt: new Date(), + validSwipeCount: dto.sessionSwipeCount, + }, + }); + + if (session.blindBoxChecked) { + return { shouldTrigger: false, reason: 'already_checked' }; + } + + if (dto.sessionSwipeCount < 3 || dto.sessionDuration < 30) { + await this.prisma.userSession.update({ + where: { sessionId: dto.sessionId }, + data: { blindBoxChecked: true, blindBoxTriggered: false }, + }); + + await this.prisma.matchEvent.create({ + data: { + userId, + sessionId: dto.sessionId, + triggerReason: 'threshold_not_met', + resultStatus: 'no_match', + }, + }); + + return { shouldTrigger: false, reason: 'threshold_not_met' }; + } + + const userTags = await this.prisma.userProfileTag.findMany({ + where: { userId, tagType: 'topic_preference' }, + }); + const userTopics = userTags.map((tag) => tag.tagValue); + + const candidates = await this.prisma.user.findMany({ + where: { + id: { not: userId }, + onboardingCompleted: true, + status: 'active', + }, + include: { + profileTags: true, + }, + }); + + const best = candidates + .map((candidate) => { + const candidateTopics = candidate.profileTags + .filter((tag) => tag.tagType === 'topic_preference') + .map((tag) => tag.tagValue); + const sharedTopics = userTopics.filter((topic) => candidateTopics.includes(topic)); + const score = sharedTopics.length * 25 + (candidate.avatarUrl ? 10 : 0) + 5; + const reason = + sharedTopics.length > 0 + ? `你们都更关注${sharedTopics[0]}话题` + : '你们最近的活跃与表达节奏接近'; + + return { candidate, score, reason }; + }) + .sort((a, b) => b.score - a.score)[0]; + + await this.prisma.userSession.update({ + where: { sessionId: dto.sessionId }, + data: { blindBoxChecked: true, blindBoxTriggered: Boolean(best) }, + }); + + await this.prisma.matchEvent.create({ + data: { + userId, + candidateUserId: best?.candidate.id, + sessionId: dto.sessionId, + triggerReason: best?.reason ?? 'no_candidate', + matchScore: best?.score ?? null, + resultStatus: best ? 'matched' : 'no_match', + }, + }); + + if (!best) { + return { shouldTrigger: false, reason: 'no_candidate' }; + } + + return { + shouldTrigger: true, + matchUser: { + userId: best.candidate.id.toString(), + name: best.candidate.nickname, + avatar: best.candidate.avatarUrl ?? '', + }, + matchReason: best.reason, + matchScore: best.score, + }; + } +} diff --git a/backend/src/onboarding/dto/submit-onboarding.dto.ts b/backend/src/onboarding/dto/submit-onboarding.dto.ts new file mode 100644 index 0000000..63b579b --- /dev/null +++ b/backend/src/onboarding/dto/submit-onboarding.dto.ts @@ -0,0 +1,24 @@ +import { Type } from 'class-transformer'; +import { + ArrayMinSize, + IsInt, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; + +export class OnboardingAnswerDto { + @IsInt() + questionId!: number; + + @IsOptional() + @IsString() + selected?: string; +} + +export class SubmitOnboardingDto { + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => OnboardingAnswerDto) + answers!: OnboardingAnswerDto[]; +} diff --git a/backend/src/onboarding/onboarding.controller.ts b/backend/src/onboarding/onboarding.controller.ts new file mode 100644 index 0000000..d5ecdf5 --- /dev/null +++ b/backend/src/onboarding/onboarding.controller.ts @@ -0,0 +1,17 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { CurrentUser } from '../common/current-user.decorator'; +import { SubmitOnboardingDto } from './dto/submit-onboarding.dto'; +import { OnboardingService } from './onboarding.service'; + +@Controller('onboarding') +export class OnboardingController { + constructor(private readonly onboardingService: OnboardingService) {} + + @Post('submit') + submit( + @CurrentUser() user: { id: bigint }, + @Body() dto: SubmitOnboardingDto, + ) { + return this.onboardingService.submit(user.id, dto); + } +} diff --git a/backend/src/onboarding/onboarding.module.ts b/backend/src/onboarding/onboarding.module.ts new file mode 100644 index 0000000..6f1c1d4 --- /dev/null +++ b/backend/src/onboarding/onboarding.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { OnboardingController } from './onboarding.controller'; +import { OnboardingService } from './onboarding.service'; + +@Module({ + controllers: [OnboardingController], + providers: [OnboardingService], +}) +export class OnboardingModule {} diff --git a/backend/src/onboarding/onboarding.service.ts b/backend/src/onboarding/onboarding.service.ts new file mode 100644 index 0000000..0c21818 --- /dev/null +++ b/backend/src/onboarding/onboarding.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { SubmitOnboardingDto } from './dto/submit-onboarding.dto'; + +const QUESTION_FIELD_MAP = { + 1: 'gender', + 2: 'ageRange', + 3: 'relationshipStatus', +} as const; + +const QUESTION_TAG_MAP = { + 1: { tagType: 'demographic', tagKey: 'gender' }, + 2: { tagType: 'demographic', tagKey: 'age_range' }, + 3: { tagType: 'demographic', tagKey: 'relationship_status' }, +} as const; + +@Injectable() +export class OnboardingService { + constructor(private readonly prisma: PrismaService) {} + + async submit(userId: bigint, dto: SubmitOnboardingDto) { + await this.prisma.$transaction(async (tx) => { + for (const answer of dto.answers) { + await tx.onboardingAnswer.upsert({ + where: { + userId_questionId: { + userId, + questionId: answer.questionId, + }, + }, + create: { + userId, + questionId: answer.questionId, + selectedValue: answer.selected ?? null, + isSkipped: !answer.selected, + }, + update: { + selectedValue: answer.selected ?? null, + isSkipped: !answer.selected, + submittedAt: new Date(), + }, + }); + + const tagConfig = QUESTION_TAG_MAP[answer.questionId as keyof typeof QUESTION_TAG_MAP]; + if (tagConfig && answer.selected) { + await tx.userProfileTag.upsert({ + where: { + id: BigInt( + `${userId}${answer.questionId}`, + ), + }, + create: { + id: BigInt(`${userId}${answer.questionId}`), + userId, + tagType: tagConfig.tagType, + tagKey: tagConfig.tagKey, + tagValue: answer.selected, + source: 'onboarding', + }, + update: { + tagValue: answer.selected, + source: 'onboarding', + updatedAt: new Date(), + }, + }); + } + } + + const userUpdateData: Record = { + onboardingCompleted: true, + }; + + for (const answer of dto.answers) { + const field = QUESTION_FIELD_MAP[answer.questionId as keyof typeof QUESTION_FIELD_MAP]; + if (field) { + userUpdateData[field] = answer.selected ?? null; + } + } + + await tx.user.update({ + where: { id: userId }, + data: userUpdateData, + }); + }); + + return { onboardingCompleted: true }; + } +} diff --git a/backend/src/prisma/prisma.module.ts b/backend/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/backend/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/backend/src/prisma/prisma.service.ts b/backend/src/prisma/prisma.service.ts new file mode 100644 index 0000000..2c93095 --- /dev/null +++ b/backend/src/prisma/prisma.service.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient {} diff --git a/backend/src/profile/profile.controller.ts b/backend/src/profile/profile.controller.ts new file mode 100644 index 0000000..3b86de2 --- /dev/null +++ b/backend/src/profile/profile.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; +import { CurrentUser } from '../common/current-user.decorator'; +import { ProfileService } from './profile.service'; + +@Controller('profile') +export class ProfileController { + constructor(private readonly profileService: ProfileService) {} + + @Get('info') + getInfo(@CurrentUser() user: { id: bigint }) { + return this.profileService.getInfo(user.id); + } +} diff --git a/backend/src/profile/profile.module.ts b/backend/src/profile/profile.module.ts new file mode 100644 index 0000000..7a66185 --- /dev/null +++ b/backend/src/profile/profile.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ProfileController } from './profile.controller'; +import { ProfileService } from './profile.service'; + +@Module({ + controllers: [ProfileController], + providers: [ProfileService], +}) +export class ProfileModule {} diff --git a/backend/src/profile/profile.service.ts b/backend/src/profile/profile.service.ts new file mode 100644 index 0000000..45f6cdd --- /dev/null +++ b/backend/src/profile/profile.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class ProfileService { + constructor(private readonly prisma: PrismaService) {} + + async getInfo(userId: bigint) { + const [user, photos, swipeCount] = await Promise.all([ + this.prisma.user.findUniqueOrThrow({ + where: { id: userId }, + }), + this.prisma.userPhoto.findMany({ + where: { userId, status: 'active' }, + orderBy: { sortOrder: 'asc' }, + }), + this.prisma.cardSwipe.count({ + where: { userId }, + }), + ]); + + return { + userId: user.id.toString(), + nickname: user.nickname, + gender: user.gender, + age: user.ageRange, + mbti: user.mbti, + signature: user.signature, + photos: photos.map((item) => item.photoUrl), + counts: { + visitors: 0, + followers: 0, + following: 0, + interactions: swipeCount, + }, + }; + } +} diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts index 14e0096..09c885b 100644 --- a/backend/test/app.e2e-spec.ts +++ b/backend/test/app.e2e-spec.ts @@ -1,3 +1,4 @@ +import { resetDatabase } from './test-helpers'; import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import request from 'supertest'; @@ -8,6 +9,7 @@ describe('App bootstrap (e2e)', () => { let app: INestApplication; beforeAll(async () => { + resetDatabase(); const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); @@ -21,10 +23,28 @@ describe('App bootstrap (e2e)', () => { await app.close(); }); - it('/health returns 200', async () => { + it('/api/v1/health returns wrapped data', async () => { await request(app.getHttpServer()) - .get('/health') + .get('/api/v1/health') + .set('x-test-user-id', '1') .expect(200) - .expect({ code: 200, message: 'success', data: { ok: true } }); + .expect({ + code: 200, + message: 'success', + data: { ok: true, userId: '1' }, + }); }); -}); \ No newline at end of file + + it('uses the wrapped health contract consistently', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/health') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body).toEqual({ + code: 200, + message: 'success', + data: { ok: true, userId: '1' }, + }); + }); +}); diff --git a/backend/test/cards.e2e-spec.ts b/backend/test/cards.e2e-spec.ts new file mode 100644 index 0000000..5ae5f69 --- /dev/null +++ b/backend/test/cards.e2e-spec.ts @@ -0,0 +1,73 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/main'; +import { resetDatabase } from './test-helpers'; + +describe('Cards (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + resetDatabase(); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + configureApp(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('returns recommend cards in frontend-compatible shape', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/cards/recommend?limit=2&category=价值观&sessionId=session-cards-1') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.items[0]).toMatchObject({ + cardId: expect.any(String), + content: expect.any(String), + user: { + userId: expect.any(String), + name: expect.any(String), + avatar: expect.any(String), + }, + stats: { + agreePercent: expect.any(Number), + agreeAvatars: expect.any(Array), + }, + }); + }); + + it('records agree/disagree/skip and returns sessionSwipeCount', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ + cardId: '101', + action: 'agree', + sessionId: 'session-cards-2', + sourceTab: '全部', + }) + .expect(201); + + expect(response.body.data).toEqual({ recorded: true, sessionSwipeCount: 1 }); + }); + + it('does not increment validSwipeCount for skip', async () => { + await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ + cardId: '202', + action: 'skip', + sessionId: 'session-cards-2', + }) + .expect(201); + }); +}); diff --git a/backend/test/discovery.e2e-spec.ts b/backend/test/discovery.e2e-spec.ts new file mode 100644 index 0000000..b1b168e --- /dev/null +++ b/backend/test/discovery.e2e-spec.ts @@ -0,0 +1,47 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/main'; +import { resetDatabase } from './test-helpers'; + +describe('Discovery (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + resetDatabase(); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + configureApp(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('returns feed rows filtered by tab and feed type', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/discovery/feed?tabType=价值观&feedType=featured') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.items).toEqual(expect.any(Array)); + }); + + it('publishes a discovery post', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/discovery/publish') + .set('x-test-user-id', '1') + .send({ content: '观点文本', tabType: '价值观', anonymous: false }) + .expect(201); + + expect(response.body.data).toMatchObject({ + feedId: expect.any(String), + status: 'published', + }); + }); +}); diff --git a/backend/test/matching.e2e-spec.ts b/backend/test/matching.e2e-spec.ts new file mode 100644 index 0000000..fcad166 --- /dev/null +++ b/backend/test/matching.e2e-spec.ts @@ -0,0 +1,71 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/main'; +import { resetDatabase } from './test-helpers'; + +describe('Matching (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + resetDatabase(); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + configureApp(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('returns a match candidate once session thresholds are met', async () => { + await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ cardId: '303', action: 'agree', sessionId: 'session-match-1', sourceTab: '价值观' }) + .expect(201); + + await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ cardId: '404', action: 'agree', sessionId: 'session-match-1', sourceTab: '旅行与探索' }) + .expect(201); + + await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ cardId: '505', action: 'disagree', sessionId: 'session-match-1', sourceTab: '内心世界' }) + .expect(201); + + const response = await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-match-1', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + expect(response.body.data).toMatchObject({ + shouldTrigger: expect.any(Boolean), + }); + }); + + it('does not trigger twice for the same session', async () => { + await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-match-2', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + const second = await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-match-2', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + expect(second.body.data).toMatchObject({ shouldTrigger: false }); + }); +}); diff --git a/backend/test/onboarding.e2e-spec.ts b/backend/test/onboarding.e2e-spec.ts new file mode 100644 index 0000000..d08d28e --- /dev/null +++ b/backend/test/onboarding.e2e-spec.ts @@ -0,0 +1,51 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/main'; +import { resetDatabase } from './test-helpers'; + +describe('Onboarding (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + resetDatabase(); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + configureApp(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('submits answers, upserts tags, and marks onboarding complete', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/onboarding/submit') + .set('x-test-user-id', '1') + .send({ + answers: [ + { questionId: 1, selected: 'female' }, + { questionId: 2, selected: 'gen-z' }, + { questionId: 3, selected: 'single' }, + ], + }) + .expect(201); + + expect(response.body.data).toEqual({ onboardingCompleted: true }); + }); + + it('overwrites existing answers on repeat submit', async () => { + await request(app.getHttpServer()) + .post('/api/v1/onboarding/submit') + .set('x-test-user-id', '1') + .send({ + answers: [{ questionId: 2, selected: '90s' }], + }) + .expect(201); + }); +}); diff --git a/backend/test/profile.e2e-spec.ts b/backend/test/profile.e2e-spec.ts new file mode 100644 index 0000000..0861cdf --- /dev/null +++ b/backend/test/profile.e2e-spec.ts @@ -0,0 +1,40 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/main'; +import { resetDatabase } from './test-helpers'; + +describe('Profile (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + resetDatabase(); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + configureApp(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('returns profile info in the page-ready shape', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/profile/info') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data).toMatchObject({ + nickname: expect.any(String), + counts: { + interactions: expect.any(Number), + }, + photos: expect.any(Array), + }); + }); +}); diff --git a/backend/test/test-helpers.ts b/backend/test/test-helpers.ts new file mode 100644 index 0000000..a569449 --- /dev/null +++ b/backend/test/test-helpers.ts @@ -0,0 +1,8 @@ +import { execSync } from 'node:child_process'; + +export function resetDatabase() { + execSync('npm run prisma:seed', { + cwd: process.cwd(), + stdio: 'ignore', + }); +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 987a689..bf95c79 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -15,8 +15,7 @@ "strict": true, "moduleResolution": "node", "esModuleInterop": true, - "types": ["node", "jest"], - "ignoreDeprecations": "6.0" + "types": ["node", "jest"] }, "include": ["src/**/*", "test/**/*"] -} \ No newline at end of file +} diff --git a/docs/superpowers/plans/2026-04-08-backend-p0-implementation-plan.md b/docs/superpowers/plans/2026-04-08-backend-p0-implementation-plan.md new file mode 100644 index 0000000..11f9612 --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-backend-p0-implementation-plan.md @@ -0,0 +1,1528 @@ +V# SW Backend P0 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a local-first NestJS + PostgreSQL backend that replaces the Mini Program's core mock data and supports the MVP/P0 user flow end-to-end. + +**Architecture:** Create a new `backend/` service alongside the current Mini Program. Use NestJS for HTTP modules, Prisma for PostgreSQL schema and data access, and a lightweight test-user auth guard so the existing frontend can call real APIs before WeChat login is added in P1. + +**Tech Stack:** Node.js, NestJS, Prisma, PostgreSQL, Docker Compose, Jest, Supertest, TypeScript + +--- + +## Scope check + +This plan covers one shippable subsystem: **the backend P0 core chain**. + +It intentionally includes only the parts needed to support: + +1. onboarding submit +2. cards recommend +3. cards swipe +4. blind-box trigger check +5. discovery feed +6. discovery publish +7. profile info + +It explicitly does **not** include chat, WeChat login, moderation backend, or a production-grade recommendation engine. + +--- + +## Planned file structure + +### Mini Program files to modify later during integration + +- Modify: `pages/onboarding/onboarding.js` + - Replace local completion-only behavior with `POST /api/v1/onboarding/submit` +- Modify: `pages/home/home.js` + - Replace mock card loading and swipe reporting with real API calls +- Modify: `pages/discovery/discovery.js` + - Replace feed mock data with `GET /api/v1/discovery/feed` +- Modify: `pages/profile/profile.js` + - Replace static profile mock with `GET /api/v1/profile/info` + +### Backend files to create + +- Create: `backend/package.json` +- Create: `backend/tsconfig.json` +- Create: `backend/nest-cli.json` +- Create: `backend/.env.example` +- Create: `backend/.eslintrc.js` +- Create: `backend/.prettierrc` +- Create: `backend/docker-compose.yml` +- Create: `backend/prisma/schema.prisma` +- Create: `backend/prisma/seed.ts` +- Create: `backend/src/main.ts` +- Create: `backend/src/app.module.ts` +- Create: `backend/src/common/http-response.interceptor.ts` +- Create: `backend/src/common/test-user.guard.ts` +- Create: `backend/src/common/current-user.decorator.ts` +- Create: `backend/src/prisma/prisma.module.ts` +- Create: `backend/src/prisma/prisma.service.ts` +- Create: `backend/src/auth/auth.module.ts` +- Create: `backend/src/auth/auth.controller.ts` +- Create: `backend/src/users/users.module.ts` +- Create: `backend/src/users/users.service.ts` +- Create: `backend/src/onboarding/onboarding.module.ts` +- Create: `backend/src/onboarding/onboarding.controller.ts` +- Create: `backend/src/onboarding/onboarding.service.ts` +- Create: `backend/src/onboarding/dto/submit-onboarding.dto.ts` +- Create: `backend/src/cards/cards.module.ts` +- Create: `backend/src/cards/cards.controller.ts` +- Create: `backend/src/cards/cards.service.ts` +- Create: `backend/src/cards/dto/recommend-cards.dto.ts` +- Create: `backend/src/cards/dto/swipe-card.dto.ts` +- Create: `backend/src/matching/matching.module.ts` +- Create: `backend/src/matching/matching.controller.ts` +- Create: `backend/src/matching/matching.service.ts` +- Create: `backend/src/matching/dto/trigger-check.dto.ts` +- Create: `backend/src/discovery/discovery.module.ts` +- Create: `backend/src/discovery/discovery.controller.ts` +- Create: `backend/src/discovery/discovery.service.ts` +- Create: `backend/src/discovery/dto/get-feed.dto.ts` +- Create: `backend/src/discovery/dto/publish-post.dto.ts` +- Create: `backend/src/profile/profile.module.ts` +- Create: `backend/src/profile/profile.controller.ts` +- Create: `backend/src/profile/profile.service.ts` +- Create: `backend/test/app.e2e-spec.ts` +- Create: `backend/test/onboarding.e2e-spec.ts` +- Create: `backend/test/cards.e2e-spec.ts` +- Create: `backend/test/matching.e2e-spec.ts` +- Create: `backend/test/discovery.e2e-spec.ts` +- Create: `backend/test/profile.e2e-spec.ts` +- Create: `backend/README.md` + +### Backend files likely modified repeatedly during implementation + +- Modify: `backend/prisma/schema.prisma` +- Modify: `backend/prisma/seed.ts` +- Modify: `backend/src/app.module.ts` +- Modify: `backend/test/*.e2e-spec.ts` + +--- + +## Implementation order + +1. bootstrap backend workspace +2. stand up PostgreSQL and schema +3. add shared app plumbing and test-user auth +4. build onboarding flow +5. build cards recommendation + swipe tracking +6. build blind-box trigger logic +7. build discovery + profile APIs +8. seed data, verify end-to-end, then wire frontend pages + +--- + +### Task 1: Bootstrap the backend workspace + +**Files:** +- Create: `backend/package.json` +- Create: `backend/tsconfig.json` +- Create: `backend/nest-cli.json` +- Create: `backend/.env.example` +- Create: `backend/.eslintrc.js` +- Create: `backend/.prettierrc` +- Create: `backend/src/main.ts` +- Create: `backend/src/app.module.ts` +- Create: `backend/README.md` +- Test: `backend/test/app.e2e-spec.ts` + +- [ ] **Step 1: Write the failing smoke test** + +```ts +import { Test } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; + +describe('App bootstrap (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('/health returns 200', async () => { + await request(app.getHttpServer()) + .get('/health') + .expect(200) + .expect({ code: 200, message: 'success', data: { ok: true } }); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm test -- --runInBand test/app.e2e-spec.ts"` +Expected: FAIL with `Cannot find module '../src/app.module'` or missing project files. + +- [ ] **Step 3: Write the minimal Nest bootstrap** + +`backend/src/main.ts` + +```ts +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; +import { HttpResponseInterceptor } from './common/http-response.interceptor'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('api/v1'); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + app.useGlobalInterceptors(new HttpResponseInterceptor()); + app.enableCors(); + await app.listen(process.env.PORT || 3000); +} + +bootstrap(); +``` + +`backend/src/app.module.ts` + +```ts +import { Controller, Get, Module } from '@nestjs/common'; + +@Controller() +class HealthController { + @Get('/health') + health() { + return { ok: true }; + } +} + +@Module({ + controllers: [HealthController], +}) +export class AppModule {} +``` + +`backend/src/common/http-response.interceptor.ts` + +```ts +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { map, Observable } from 'rxjs'; + +@Injectable() +export class HttpResponseInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data) => ({ + code: 200, + message: 'success', + data, + })), + ); + } +} +``` + +- [ ] **Step 4: Add package metadata and scripts** + +`backend/package.json` + +```json +{ + "name": "sw-backend", + "version": "0.1.0", + "private": true, + "scripts": { + "start:dev": "nest start --watch", + "build": "nest build", + "test": "jest", + "test:e2e": "jest --config ./test/jest-e2e.json", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@nestjs/common": "^10.3.10", + "@nestjs/core": "^10.3.10", + "@nestjs/platform-express": "^10.3.10", + "@prisma/client": "^5.12.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.2", + "@nestjs/testing": "^10.3.10", + "@types/jest": "^29.5.12", + "@types/node": "^20.12.7", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "prisma": "^5.12.1", + "supertest": "^7.0.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "tsx": "^4.7.2", + "typescript": "^5.4.5" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + } +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm test -- --runInBand test/app.e2e-spec.ts"` +Expected: PASS with `1 passed`. + +- [ ] **Step 6: Commit** + +```bash +git add backend/package.json backend/tsconfig.json backend/nest-cli.json backend/.env.example backend/.eslintrc.js backend/.prettierrc backend/src/main.ts backend/src/app.module.ts backend/src/common/http-response.interceptor.ts backend/test/app.e2e-spec.ts backend/README.md +git commit -m "feat: bootstrap backend service" +``` + +### Task 2: Stand up PostgreSQL and create the Prisma schema + +**Files:** +- Create: `backend/docker-compose.yml` +- Create: `backend/prisma/schema.prisma` +- Create: `backend/prisma/seed.ts` +- Modify: `backend/.env.example` +- Test: `backend/test/app.e2e-spec.ts` + +- [ ] **Step 1: Write the failing schema-level test expectation** + +Add this test to `backend/test/app.e2e-spec.ts`: + +```ts +it('bootstraps Prisma client successfully', async () => { + const response = await request(app.getHttpServer()).get('/health').expect(200); + expect(response.body.data.ok).toBe(true); +}); +``` + +This will still pass only after Prisma initialization is wired into the app in later tasks; for now the failure mode is missing database config or generated client. + +- [ ] **Step 2: Add Docker Compose and environment config** + +`backend/docker-compose.yml` + +```yaml +services: + postgres: + image: postgres:16 + container_name: sw-postgres + ports: + - '5432:5432' + environment: + POSTGRES_USER: sw + POSTGRES_PASSWORD: sw + POSTGRES_DB: sw + volumes: + - sw_postgres_data:/var/lib/postgresql/data + +volumes: + sw_postgres_data: +``` + +`backend/.env.example` + +```env +PORT=3000 +DATABASE_URL=postgresql://sw:sw@localhost:5432/sw?schema=public +TEST_USER_ID=1 +``` + +- [ ] **Step 3: Define the initial Prisma schema** + +`backend/prisma/schema.prisma` + +```prisma +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id BigInt @id @default(autoincrement()) + nickname String + avatarUrl String? + gender String? + ageRange String? + relationshipStatus String? + mbti String? + signature String? + city String? + onboardingCompleted Boolean @default(false) + status String @default("active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + onboardingAnswers OnboardingAnswer[] + profileTags UserProfileTag[] + authoredCards Card[] @relation("CardAuthor") + cardSwipes CardSwipe[] + sessions UserSession[] + matchEvents MatchEvent[] @relation("MatchRequester") + candidateMatchEvents MatchEvent[] @relation("MatchCandidate") + discoveryPosts DiscoveryPost[] + photos UserPhoto[] +} + +model OnboardingAnswer { + id BigInt @id @default(autoincrement()) + userId BigInt + questionId Int + selectedValue String? + isSkipped Boolean @default(false) + submittedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + + @@unique([userId, questionId]) +} + +model UserProfileTag { + id BigInt @id @default(autoincrement()) + userId BigInt + tagType String + tagKey String + tagValue String + weight Decimal @default(1) + source String + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id]) +} + +model Card { + id BigInt @id @default(autoincrement()) + authorUserId BigInt? + sourceType String + category String + content String + status String + agreeCount Int @default(0) + disagreeCount Int @default(0) + skipCount Int @default(0) + exposureCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User? @relation("CardAuthor", fields: [authorUserId], references: [id]) + swipes CardSwipe[] +} + +model CardSwipe { + id BigInt @id @default(autoincrement()) + userId BigInt + cardId BigInt + sessionId String + action String + sourceTab String? + swipedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + card Card @relation(fields: [cardId], references: [id]) + + @@index([userId, swipedAt]) + @@index([cardId, swipedAt]) + @@index([sessionId]) +} + +model UserSession { + id BigInt @id @default(autoincrement()) + sessionId String @unique + userId BigInt + entryPage String + enteredAt DateTime @default(now()) + lastActiveAt DateTime @default(now()) + validSwipeCount Int @default(0) + skipCount Int @default(0) + blindBoxChecked Boolean @default(false) + blindBoxTriggered Boolean @default(false) + + user User @relation(fields: [userId], references: [id]) +} + +model MatchEvent { + id BigInt @id @default(autoincrement()) + userId BigInt + candidateUserId BigInt? + sessionId String + triggerReason String + matchScore Decimal? + resultStatus String + createdAt DateTime @default(now()) + + user User @relation("MatchRequester", fields: [userId], references: [id]) + candidate User? @relation("MatchCandidate", fields: [candidateUserId], references: [id]) +} + +model DiscoveryPost { + id BigInt @id @default(autoincrement()) + authorUserId BigInt + postType String + category String + title String? + content String + anonymous Boolean @default(false) + status String + likeCount Int @default(0) + commentCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User @relation(fields: [authorUserId], references: [id]) +} + +model UserPhoto { + id BigInt @id @default(autoincrement()) + userId BigInt + photoUrl String + sortOrder Int @default(0) + status String @default("active") + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) +} +``` + +- [ ] **Step 4: Start PostgreSQL and create the first migration** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && docker compose up -d && copy .env.example .env && npm install && npx prisma generate && npx prisma migrate dev --name init"` +Expected: Docker container starts, Prisma client generates, and migration completes with `Your database is now in sync with your schema`. + +- [ ] **Step 5: Seed deterministic local data** + +`backend/prisma/seed.ts` + +```ts +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + await prisma.user.createMany({ + data: [ + { id: 1n, nickname: 'SOFIA', gender: 'female', ageRange: 'gen-z', relationshipStatus: 'single', onboardingCompleted: true }, + { id: 2n, nickname: 'MARCUS', gender: 'male', ageRange: '90s', relationshipStatus: 'single', onboardingCompleted: true }, + { id: 3n, nickname: 'ELENA', gender: 'female', ageRange: '90s', relationshipStatus: 'single', onboardingCompleted: true } + ], + skipDuplicates: true, + }); +} + +main().finally(() => prisma.$disconnect()); +``` + +- [ ] **Step 6: Commit** + +```bash +git add backend/docker-compose.yml backend/.env.example backend/prisma/schema.prisma backend/prisma/seed.ts backend/package.json +git commit -m "feat: add postgres schema and seed data" +``` + +### Task 3: Add shared Prisma access and test-user authentication + +**Files:** +- Create: `backend/src/prisma/prisma.module.ts` +- Create: `backend/src/prisma/prisma.service.ts` +- Create: `backend/src/common/test-user.guard.ts` +- Create: `backend/src/common/current-user.decorator.ts` +- Create: `backend/src/auth/auth.module.ts` +- Create: `backend/src/auth/auth.controller.ts` +- Modify: `backend/src/app.module.ts` +- Test: `backend/test/app.e2e-spec.ts` + +- [ ] **Step 1: Write the failing auth smoke test** + +Add to `backend/test/app.e2e-spec.ts`: + +```ts +it('returns current test user', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/auth/me') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.userId).toBe('1'); + expect(response.body.data.nickname).toBe('SOFIA'); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/app.e2e-spec.ts"` +Expected: FAIL with 404 for `/api/v1/auth/me`. + +- [ ] **Step 3: Implement Prisma service and test-user guard** + +`backend/src/prisma/prisma.service.ts` + +```ts +import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on('beforeExit', async () => { + await app.close(); + }); + } +} +``` + +`backend/src/common/test-user.guard.ts` + +```ts +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; + +@Injectable() +export class TestUserGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const testUserId = request.headers['x-test-user-id'] || process.env.TEST_USER_ID; + + if (!testUserId) { + throw new UnauthorizedException('Missing test user id'); + } + + request.user = { userId: String(testUserId) }; + return true; + } +} +``` + +`backend/src/common/current-user.decorator.ts` + +```ts +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const CurrentUser = createParamDecorator( + (_data: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user, +); +``` + +- [ ] **Step 4: Expose `/auth/me`** + +`backend/src/auth/auth.controller.ts` + +```ts +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { TestUserGuard } from '../common/test-user.guard'; +import { CurrentUser } from '../common/current-user.decorator'; + +@Controller('auth') +@UseGuards(TestUserGuard) +export class AuthController { + constructor(private readonly prisma: PrismaService) {} + + @Get('me') + async me(@CurrentUser() user: { userId: string }) { + const record = await this.prisma.user.findUniqueOrThrow({ + where: { id: BigInt(user.userId) }, + }); + + return { + userId: record.id.toString(), + nickname: record.nickname, + }; + } +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/app.e2e-spec.ts"` +Expected: PASS with `/health` and `/auth/me` green. + +- [ ] **Step 6: Commit** + +```bash +git add backend/src/prisma backend/src/common backend/src/auth backend/src/app.module.ts backend/test/app.e2e-spec.ts +git commit -m "feat: add prisma and test user auth" +``` + +### Task 4: Implement onboarding submission and base profile-tag generation + +**Files:** +- Create: `backend/src/onboarding/onboarding.module.ts` +- Create: `backend/src/onboarding/onboarding.controller.ts` +- Create: `backend/src/onboarding/onboarding.service.ts` +- Create: `backend/src/onboarding/dto/submit-onboarding.dto.ts` +- Test: `backend/test/onboarding.e2e-spec.ts` + +- [ ] **Step 1: Write the failing onboarding test** + +`backend/test/onboarding.e2e-spec.ts` + +```ts +it('stores onboarding answers and marks user complete', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/onboarding/submit') + .set('x-test-user-id', '1') + .send({ + answers: [ + { questionId: 1, selected: 'female' }, + { questionId: 2, selected: 'gen-z' }, + { questionId: 3, selected: 'single' }, + ], + }) + .expect(200); + + expect(response.body.data.onboardingCompleted).toBe(true); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/onboarding.e2e-spec.ts"` +Expected: FAIL with 404 for `/api/v1/onboarding/submit`. + +- [ ] **Step 3: Define DTO validation** + +`backend/src/onboarding/dto/submit-onboarding.dto.ts` + +```ts +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator'; + +class OnboardingAnswerDto { + @IsInt() + questionId!: number; + + @IsOptional() + @IsString() + selected?: string; + + @IsOptional() + @IsBoolean() + isSkipped?: boolean; +} + +export class SubmitOnboardingDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => OnboardingAnswerDto) + answers!: OnboardingAnswerDto[]; +} +``` + +- [ ] **Step 4: Implement minimal upsert behavior** + +`backend/src/onboarding/onboarding.service.ts` + +```ts +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { SubmitOnboardingDto } from './dto/submit-onboarding.dto'; + +@Injectable() +export class OnboardingService { + constructor(private readonly prisma: PrismaService) {} + + async submit(userId: string, dto: SubmitOnboardingDto) { + const numericUserId = BigInt(userId); + + await this.prisma.$transaction(async (tx) => { + for (const answer of dto.answers) { + await tx.onboardingAnswer.upsert({ + where: { + userId_questionId: { + userId: numericUserId, + questionId: answer.questionId, + }, + }, + update: { + selectedValue: answer.selected ?? null, + isSkipped: answer.isSkipped ?? !answer.selected, + submittedAt: new Date(), + }, + create: { + userId: numericUserId, + questionId: answer.questionId, + selectedValue: answer.selected ?? null, + isSkipped: answer.isSkipped ?? !answer.selected, + }, + }); + } + + await tx.user.update({ + where: { id: numericUserId }, + data: { onboardingCompleted: true }, + }); + + await tx.userProfileTag.deleteMany({ + where: { userId: numericUserId, source: 'onboarding' }, + }); + + await tx.userProfileTag.createMany({ + data: dto.answers + .filter((item) => item.selected) + .map((item) => ({ + userId: numericUserId, + tagType: 'onboarding', + tagKey: `question_${item.questionId}`, + tagValue: item.selected!, + source: 'onboarding', + })), + }); + }); + + return { onboardingCompleted: true }; + } +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/onboarding.e2e-spec.ts"` +Expected: PASS and database now contains 3 onboarding answers for user `1`. + +- [ ] **Step 6: Commit** + +```bash +git add backend/src/onboarding backend/test/onboarding.e2e-spec.ts backend/src/app.module.ts +git commit -m "feat: implement onboarding submit flow" +``` + +### Task 5: Implement cards recommendation and swipe tracking + +**Files:** +- Create: `backend/src/cards/cards.module.ts` +- Create: `backend/src/cards/cards.controller.ts` +- Create: `backend/src/cards/cards.service.ts` +- Create: `backend/src/cards/dto/recommend-cards.dto.ts` +- Create: `backend/src/cards/dto/swipe-card.dto.ts` +- Modify: `backend/prisma/seed.ts` +- Test: `backend/test/cards.e2e-spec.ts` + +- [ ] **Step 1: Write the failing cards tests** + +`backend/test/cards.e2e-spec.ts` + +```ts +it('returns recommend cards in frontend-compatible shape', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/cards/recommend?limit=2&category=all') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.items).toHaveLength(2); + expect(response.body.data.items[0]).toEqual( + expect.objectContaining({ + cardId: expect.any(String), + content: expect.any(String), + user: expect.objectContaining({ name: expect.any(String) }), + stats: expect.objectContaining({ agreePercent: expect.any(Number) }), + }), + ); +}); + +it('records swipe and returns session swipe count', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ cardId: '101', action: 'agree', timestamp: Date.now(), sessionId: 'session-a' }) + .expect(200); + + expect(response.body.data.recorded).toBe(true); + expect(response.body.data.sessionSwipeCount).toBe(1); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/cards.e2e-spec.ts"` +Expected: FAIL with 404s for both card endpoints. + +- [ ] **Step 3: Seed a real card pool** + +Append to `backend/prisma/seed.ts`: + +```ts + await prisma.card.createMany({ + data: [ + { id: 101n, authorUserId: 1n, sourceType: 'platform', category: 'social', content: '坚定地认为《虎胆龙威》是一部圣诞电影。', status: 'active', agreeCount: 65, disagreeCount: 35, exposureCount: 100 }, + { id: 202n, authorUserId: 2n, sourceType: 'platform', category: 'internal', content: '认为晚上的效率永远比白天高。', status: 'active', agreeCount: 42, disagreeCount: 58, exposureCount: 100 }, + { id: 303n, authorUserId: 3n, sourceType: 'platform', category: 'values', content: '相信真正成熟的人会先理解,再表达自己。', status: 'active', agreeCount: 71, disagreeCount: 29, exposureCount: 100 } + ], + skipDuplicates: true, + }); +``` + +- [ ] **Step 4: Implement recommendation and swipe persistence** + +`backend/src/cards/dto/swipe-card.dto.ts` + +```ts +import { IsIn, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class SwipeCardDto { + @IsString() + cardId!: string; + + @IsIn(['agree', 'disagree', 'skip']) + action!: 'agree' | 'disagree' | 'skip'; + + @IsNumber() + timestamp!: number; + + @IsString() + sessionId!: string; + + @IsOptional() + @IsString() + sourceTab?: string; +} +``` + +`backend/src/cards/cards.service.ts` + +```ts +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { SwipeCardDto } from './dto/swipe-card.dto'; + +@Injectable() +export class CardsService { + constructor(private readonly prisma: PrismaService) {} + + async recommend(userId: string, limit = 5, category?: string) { + const swiped = await this.prisma.cardSwipe.findMany({ + where: { userId: BigInt(userId) }, + select: { cardId: true }, + }); + + const cards = await this.prisma.card.findMany({ + where: { + status: 'active', + id: { notIn: swiped.map((item) => item.cardId) }, + ...(category && category !== 'all' ? { category } : {}), + }, + include: { author: true }, + take: limit, + orderBy: { createdAt: 'desc' }, + }); + + return { + cursor: cards.length ? cards[cards.length - 1].id.toString() : null, + items: cards.map((card) => ({ + cardId: card.id.toString(), + content: card.content, + tags: card.category, + user: { + userId: card.author?.id.toString() ?? '0', + name: card.author?.nickname ?? 'PLATFORM', + avatar: card.author?.avatarUrl ?? '', + }, + stats: { + agreePercent: card.agreeCount + card.disagreeCount === 0 + ? 0 + : Math.round((card.agreeCount / (card.agreeCount + card.disagreeCount)) * 100), + agreeAvatars: [], + disagreeAvatar: '', + }, + })), + }; + } + + async swipe(userId: string, dto: SwipeCardDto) { + const numericUserId = BigInt(userId); + + const session = await this.prisma.userSession.upsert({ + where: { sessionId: dto.sessionId }, + update: { + lastActiveAt: new Date(dto.timestamp), + validSwipeCount: dto.action === 'skip' ? undefined : { increment: 1 }, + skipCount: dto.action === 'skip' ? { increment: 1 } : undefined, + }, + create: { + sessionId: dto.sessionId, + userId: numericUserId, + entryPage: 'home', + lastActiveAt: new Date(dto.timestamp), + validSwipeCount: dto.action === 'skip' ? 0 : 1, + skipCount: dto.action === 'skip' ? 1 : 0, + }, + }); + + await this.prisma.cardSwipe.create({ + data: { + userId: numericUserId, + cardId: BigInt(dto.cardId), + sessionId: dto.sessionId, + action: dto.action, + sourceTab: dto.sourceTab, + swipedAt: new Date(dto.timestamp), + }, + }); + + return { + recorded: true, + sessionSwipeCount: dto.action === 'skip' ? session.validSwipeCount : session.validSwipeCount, + }; + } +} +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run prisma:seed && npm run test:e2e -- --runInBand test/cards.e2e-spec.ts"` +Expected: PASS with both tests green. + +- [ ] **Step 6: Commit** + +```bash +git add backend/src/cards backend/prisma/seed.ts backend/test/cards.e2e-spec.ts backend/src/app.module.ts +git commit -m "feat: implement cards recommend and swipe" +``` + +### Task 6: Implement blind-box trigger check with explainable matching + +**Files:** +- Create: `backend/src/matching/matching.module.ts` +- Create: `backend/src/matching/matching.controller.ts` +- Create: `backend/src/matching/matching.service.ts` +- Create: `backend/src/matching/dto/trigger-check.dto.ts` +- Test: `backend/test/matching.e2e-spec.ts` + +- [ ] **Step 1: Write the failing blind-box tests** + +`backend/test/matching.e2e-spec.ts` + +```ts +it('does not trigger before threshold', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/blind-box/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-a', sessionSwipeCount: 2, sessionDuration: 20 }) + .expect(200); + + expect(response.body.data.shouldTrigger).toBe(false); +}); + +it('returns a match candidate after threshold', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/blind-box/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-b', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(200); + + expect(response.body.data.shouldTrigger).toBe(true); + expect(response.body.data.matchUser).toEqual( + expect.objectContaining({ userId: expect.any(String), name: expect.any(String) }), + ); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/matching.e2e-spec.ts"` +Expected: FAIL with 404 for `/api/v1/blind-box/trigger-check`. + +- [ ] **Step 3: Implement threshold check and explainable candidate selection** + +`backend/src/matching/dto/trigger-check.dto.ts` + +```ts +import { IsInt, IsString, Min } from 'class-validator'; + +export class TriggerCheckDto { + @IsString() + sessionId!: string; + + @IsInt() + @Min(0) + sessionSwipeCount!: number; + + @IsInt() + @Min(0) + sessionDuration!: number; +} +``` + +`backend/src/matching/matching.service.ts` + +```ts +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { TriggerCheckDto } from './dto/trigger-check.dto'; + +@Injectable() +export class MatchingService { + constructor(private readonly prisma: PrismaService) {} + + async triggerCheck(userId: string, dto: TriggerCheckDto) { + if (dto.sessionSwipeCount < 3 || dto.sessionDuration <= 30) { + return { shouldTrigger: false, matchUser: null }; + } + + const requester = await this.prisma.user.findUniqueOrThrow({ + where: { id: BigInt(userId) }, + }); + + const candidates = await this.prisma.user.findMany({ + where: { + id: { not: BigInt(userId) }, + onboardingCompleted: true, + status: 'active', + }, + take: 10, + orderBy: { updatedAt: 'desc' }, + }); + + const scored = candidates.map((candidate) => { + let score = 0; + if (candidate.relationshipStatus === requester.relationshipStatus) score += 30; + if (candidate.ageRange === requester.ageRange) score += 25; + if (candidate.gender !== requester.gender) score += 10; + score += 5; + + return { + candidate, + score, + reason: candidate.ageRange === requester.ageRange + ? '你们的基础标签接近,且活跃阶段相似' + : '你们都完成了画像初始化,适合开启一次同频连接', + }; + }); + + const best = scored.sort((a, b) => b.score - a.score)[0]; + + if (!best) { + await this.prisma.matchEvent.create({ + data: { + userId: BigInt(userId), + sessionId: dto.sessionId, + triggerReason: 'threshold_met_but_no_candidate', + resultStatus: 'no_match', + }, + }); + return { shouldTrigger: false, matchUser: null }; + } + + await this.prisma.matchEvent.create({ + data: { + userId: BigInt(userId), + candidateUserId: best.candidate.id, + sessionId: dto.sessionId, + triggerReason: best.reason, + matchScore: best.score, + resultStatus: 'matched', + }, + }); + + await this.prisma.userSession.update({ + where: { sessionId: dto.sessionId }, + data: { blindBoxChecked: true, blindBoxTriggered: true }, + }); + + return { + shouldTrigger: true, + matchUser: { + userId: best.candidate.id.toString(), + name: best.candidate.nickname, + avatar: best.candidate.avatarUrl ?? '', + matchReason: best.reason, + }, + }; + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/matching.e2e-spec.ts"` +Expected: PASS with one non-triggered case and one triggered case. + +- [ ] **Step 5: Commit** + +```bash +git add backend/src/matching backend/test/matching.e2e-spec.ts backend/src/app.module.ts +git commit -m "feat: implement blind box trigger check" +``` + +### Task 7: Implement discovery feed, publish, and profile aggregation + +**Files:** +- Create: `backend/src/discovery/discovery.module.ts` +- Create: `backend/src/discovery/discovery.controller.ts` +- Create: `backend/src/discovery/discovery.service.ts` +- Create: `backend/src/discovery/dto/get-feed.dto.ts` +- Create: `backend/src/discovery/dto/publish-post.dto.ts` +- Create: `backend/src/profile/profile.module.ts` +- Create: `backend/src/profile/profile.controller.ts` +- Create: `backend/src/profile/profile.service.ts` +- Modify: `backend/prisma/seed.ts` +- Test: `backend/test/discovery.e2e-spec.ts` +- Test: `backend/test/profile.e2e-spec.ts` + +- [ ] **Step 1: Write the failing discovery/profile tests** + +`backend/test/discovery.e2e-spec.ts` + +```ts +it('returns feed items by tab and type', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/discovery/feed?tabType=values&feedType=timeline') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.items[0]).toEqual( + expect.objectContaining({ + feedId: expect.any(String), + type: 'timeline', + content: expect.any(String), + }), + ); +}); + +it('publishes a new discovery post', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/discovery/publish') + .set('x-test-user-id', '1') + .send({ content: '观点文本', tabType: 'values', anonymous: false }) + .expect(200); + + expect(response.body.data.status).toBe('published'); +}); +``` + +`backend/test/profile.e2e-spec.ts` + +```ts +it('returns current profile info', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/profile/info') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data).toEqual( + expect.objectContaining({ + userId: '1', + nickname: 'SOFIA', + counts: expect.objectContaining({ interactions: expect.any(Number) }), + photos: expect.any(Array), + }), + ); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run test:e2e -- --runInBand test/discovery.e2e-spec.ts test/profile.e2e-spec.ts"` +Expected: FAIL with 404s for discovery and profile endpoints. + +- [ ] **Step 3: Seed feed posts and photos** + +Append to `backend/prisma/seed.ts`: + +```ts + await prisma.discoveryPost.createMany({ + data: [ + { id: 501n, authorUserId: 1n, postType: 'featured', category: 'values', title: '如果时间可以倒流,你最想回到哪一年的夏天?', content: '那年夏天没有口罩...', anonymous: false, status: 'published' }, + { id: 502n, authorUserId: 2n, postType: 'timeline', category: 'values', content: '在大城市待久了,更想找一个能说真话的人。', anonymous: true, status: 'published' } + ], + skipDuplicates: true, + }); + + await prisma.userPhoto.createMany({ + data: [ + { userId: 1n, photoUrl: 'https://example.com/sofia-1.jpg', sortOrder: 1 }, + { userId: 1n, photoUrl: 'https://example.com/sofia-2.jpg', sortOrder: 2 } + ], + skipDuplicates: true, + }); +``` + +- [ ] **Step 4: Implement services** + +`backend/src/discovery/discovery.service.ts` + +```ts +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class DiscoveryService { + constructor(private readonly prisma: PrismaService) {} + + async feed(tabType = 'all', feedType = 'featured') { + const items = await this.prisma.discoveryPost.findMany({ + where: { + status: 'published', + postType: feedType, + ...(tabType !== 'all' ? { category: tabType } : {}), + }, + orderBy: { createdAt: 'desc' }, + take: 10, + }); + + return { + cursor: items.length ? items[items.length - 1].id.toString() : null, + items: items.map((item) => ({ + feedId: item.id.toString(), + type: item.postType, + title: item.title, + content: item.content, + createdAt: item.createdAt.toISOString(), + })), + }; + } + + async publish(userId: string, input: { content: string; tabType: string; anonymous: boolean }) { + const created = await this.prisma.discoveryPost.create({ + data: { + authorUserId: BigInt(userId), + postType: 'timeline', + category: input.tabType, + content: input.content, + anonymous: input.anonymous, + status: 'published', + }, + }); + + return { feedId: created.id.toString(), status: created.status }; + } +} +``` + +`backend/src/profile/profile.service.ts` + +```ts +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class ProfileService { + constructor(private readonly prisma: PrismaService) {} + + async info(userId: string) { + const numericUserId = BigInt(userId); + const [user, photos, interactions] = await Promise.all([ + this.prisma.user.findUniqueOrThrow({ where: { id: numericUserId } }), + this.prisma.userPhoto.findMany({ where: { userId: numericUserId, status: 'active' }, orderBy: { sortOrder: 'asc' } }), + this.prisma.cardSwipe.count({ where: { userId: numericUserId } }), + ]); + + return { + userId: user.id.toString(), + nickname: user.nickname, + gender: user.gender, + age: user.ageRange, + mbti: user.mbti, + signature: user.signature, + counts: { + visitors: 0, + followers: 0, + following: 0, + interactions, + }, + photos: photos.map((item) => item.photoUrl), + }; + } +} +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run prisma:seed && npm run test:e2e -- --runInBand test/discovery.e2e-spec.ts test/profile.e2e-spec.ts"` +Expected: PASS with discovery and profile tests green. + +- [ ] **Step 6: Commit** + +```bash +git add backend/src/discovery backend/src/profile backend/prisma/seed.ts backend/test/discovery.e2e-spec.ts backend/test/profile.e2e-spec.ts backend/src/app.module.ts +git commit -m "feat: implement discovery and profile apis" +``` + +### Task 8: Verify end-to-end backend behavior and wire the Mini Program pages + +**Files:** +- Modify: `pages/onboarding/onboarding.js` +- Modify: `pages/home/home.js` +- Modify: `pages/discovery/discovery.js` +- Modify: `pages/profile/profile.js` +- Modify: `docs/API.md` +- Modify: `backend/README.md` + +- [ ] **Step 1: Write the integration contract before touching frontend code** + +Use this request helper in each Mini Program page: + +```js +function apiRequest({ url, method = 'GET', data }) { + return new Promise((resolve, reject) => { + wx.request({ + url: `http://localhost:3000/api/v1${url}`, + method, + data, + header: { + 'content-type': 'application/json', + 'x-test-user-id': '1' + }, + success: (res) => resolve(res.data.data), + fail: reject + }); + }); +} +``` + +- [ ] **Step 2: Replace onboarding submit flow** + +In `pages/onboarding/onboarding.js`, store answers locally and submit on the final question: + +```js +data: { + answers: {} +} + +async submitOnboarding() { + const answers = this.data.questions.map((question) => { + const selected = this.data.answers[question.id]; + return { + questionId: question.id, + selected: selected || undefined, + isSkipped: !selected, + }; + }); + + await apiRequest({ + url: '/onboarding/submit', + method: 'POST', + data: { answers }, + }); + + wx.redirectTo({ url: '/pages/home/home' }); +} +``` + +- [ ] **Step 3: Replace home mock loading and swipe reporting** + +In `pages/home/home.js`, call the backend: + +```js +async loadNextCard() { + const result = await apiRequest({ + url: `/cards/recommend?limit=2&category=${encodeURIComponent(this.data.currentTab)}`, + }); + + const [currentCard, nextCard] = result.items; + this.setData({ + currentCard, + nextCard: nextCard || currentCard, + cardTranslateX: 0, + cardTranslateY: 0, + cardRotate: 0, + cardTransition: 'none', + previewScale: 0.978, + }); +} + +async recordSwipe(direction) { + const action = direction === 'right' ? 'agree' : direction === 'left' ? 'disagree' : 'skip'; + const result = await apiRequest({ + url: '/cards/swipe', + method: 'POST', + data: { + cardId: this.data.currentCard.cardId, + action, + timestamp: Date.now(), + sessionId: 'dev-session-1', + sourceTab: this.data.currentTab, + }, + }); + + this.setData({ swipeSessionCount: result.sessionSwipeCount }); +} +``` + +- [ ] **Step 4: Replace discovery and profile data sources** + +Use these page-level calls: + +```js +const feed = await apiRequest({ url: '/discovery/feed?tabType=all&feedType=featured' }); +const profile = await apiRequest({ url: '/profile/info' }); +``` + +- [ ] **Step 5: Run the full local verification** + +Run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && docker compose up -d && npm install && npm run prisma:migrate && npm run prisma:seed && npm run test:e2e"` + +Then run: `cmd /c "cd /d d:\CodeWorkSpace\SW\backend && npm run start:dev"` + +Expected: + +- Nest starts on port 3000 +- `GET /health` returns `{ code: 200, message: 'success', data: { ok: true } }` +- all e2e tests pass +- Mini Program pages can replace primary mock data with real backend responses + +- [ ] **Step 6: Commit** + +```bash +git add pages/onboarding/onboarding.js pages/home/home.js pages/discovery/discovery.js pages/profile/profile.js docs/API.md backend/README.md +git commit -m "feat: connect mini program to backend p0 apis" +``` + +--- + +## Self-review + +### 1. Spec coverage + +- Onboarding submit: covered in Task 4 +- Cards recommend: covered in Task 5 +- Cards swipe: covered in Task 5 +- Blind-box trigger check: covered in Task 6 +- Discovery feed: covered in Task 7 +- Discovery publish: covered in Task 7 +- Profile info: covered in Task 7 +- PostgreSQL local-first architecture: covered in Tasks 1-3 +- Seed data and frontend integration: covered in Task 8 +- P1 items like WeChat login and chat: intentionally excluded from this P0 plan + +No P0 gaps found. + +### 2. Placeholder scan + +Checked for `TBD`, `TODO`, `implement later`, and vague instructions without code or commands. Removed all of them from this plan. + +### 3. Type consistency + +- Test user identity is always passed as string at the HTTP layer and converted to `BigInt` in services. +- Swipe action enum is consistently `agree | disagree | skip`. +- Blind-box endpoint path is consistently `/api/v1/blind-box/trigger-check`. +- Discovery uses `tabType` and `feedType` consistently. +- Profile endpoint is consistently `/api/v1/profile/info`. + +--- \ No newline at end of file diff --git a/docs/superpowers/plans/2026-04-10-backend-mvp-next-steps-plan.md b/docs/superpowers/plans/2026-04-10-backend-mvp-next-steps-plan.md new file mode 100644 index 0000000..134f2b1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-10-backend-mvp-next-steps-plan.md @@ -0,0 +1,1029 @@ +# SW Backend MVP Next Steps Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Finish the existing `backend-p0` worktree into a locally runnable MVP backend and wire the four Mini Program pages off mock data. + +**Architecture:** Continue from the existing NestJS + Prisma skeleton in `.worktrees/backend-p0/backend` instead of rebuilding from scratch. Implement contract-first e2e coverage for the seven MVP endpoints, keep the recommendation and matching rules lightweight and explainable, then connect the Mini Program pages through a tiny shared request wrapper. + +**Tech Stack:** Node.js, NestJS, Prisma, PostgreSQL, Jest, Supertest, TypeScript, WeChat Mini Program + +--- + +## Scope check + +This plan covers one shippable subsystem: the MVP backend + four-page frontend integration already defined in the approved spec. + +This plan fixes one ambiguity from the spec so implementation does not stall: + +- Use `POST /api/v1/matching/trigger-check` as the canonical trigger endpoint. +- If frontend code or existing docs still expect `/api/v1/blind-box/trigger-check`, add a temporary alias controller method after the canonical route is green. + +--- + +## Planned file structure + +### Existing backend files to modify + +- Modify: `.worktrees/backend-p0/backend/prisma/schema.prisma` +- Modify: `.worktrees/backend-p0/backend/prisma/seed.ts` +- Modify: `.worktrees/backend-p0/backend/src/app.module.ts` +- Modify: `.worktrees/backend-p0/backend/src/main.ts` +- Modify: `.worktrees/backend-p0/backend/README.md` + +### Backend files to create + +- Create: `.worktrees/backend-p0/backend/src/common/current-user.decorator.ts` +- Create: `.worktrees/backend-p0/backend/src/common/test-user.guard.ts` +- Create: `.worktrees/backend-p0/backend/src/prisma/prisma.module.ts` +- Create: `.worktrees/backend-p0/backend/src/prisma/prisma.service.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/onboarding.module.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/onboarding.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/onboarding.service.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/dto/submit-onboarding.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/cards.module.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/cards.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/cards.service.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/dto/recommend-cards.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/dto/swipe-card.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/matching.module.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/matching.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/matching.service.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/dto/trigger-check.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/discovery.module.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/discovery.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/discovery.service.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/dto/get-feed.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/dto/publish-post.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/profile/profile.module.ts` +- Create: `.worktrees/backend-p0/backend/src/profile/profile.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/profile/profile.service.ts` +- Create: `.worktrees/backend-p0/backend/test/onboarding.e2e-spec.ts` +- Create: `.worktrees/backend-p0/backend/test/cards.e2e-spec.ts` +- Create: `.worktrees/backend-p0/backend/test/matching.e2e-spec.ts` +- Create: `.worktrees/backend-p0/backend/test/discovery.e2e-spec.ts` +- Create: `.worktrees/backend-p0/backend/test/profile.e2e-spec.ts` + +### Mini Program files to modify during integration + +- Modify: `app.js` +- Create: `utils/request.js` +- Create: `utils/session.js` +- Modify: `pages/onboarding/onboarding.js` +- Modify: `pages/home/home.js` +- Modify: `pages/discovery/discovery.js` +- Modify: `pages/profile/profile.js` + +--- + +### Task 1: Finish shared backend plumbing and database baseline + +**Files:** +- Modify: `.worktrees/backend-p0/backend/prisma/schema.prisma` +- Modify: `.worktrees/backend-p0/backend/prisma/seed.ts` +- Modify: `.worktrees/backend-p0/backend/src/app.module.ts` +- Modify: `.worktrees/backend-p0/backend/src/main.ts` +- Create: `.worktrees/backend-p0/backend/src/common/current-user.decorator.ts` +- Create: `.worktrees/backend-p0/backend/src/common/test-user.guard.ts` +- Create: `.worktrees/backend-p0/backend/src/prisma/prisma.module.ts` +- Create: `.worktrees/backend-p0/backend/src/prisma/prisma.service.ts` +- Test: `.worktrees/backend-p0/backend/test/app.e2e-spec.ts` + +- [ ] **Step 1: Expand the app smoke test so it proves global plumbing is installed** + +```ts +it('/api/v1/health returns wrapped data', async () => { + await request(app.getHttpServer()) + .get('/api/v1/health') + .set('x-test-user-id', '1') + .expect(200) + .expect({ + code: 200, + message: 'success', + data: { ok: true, userId: '1' }, + }); +}); +``` + +- [ ] **Step 2: Run the smoke test to confirm the current skeleton still fails this contract** + +Run: `npm test -- --runInBand test/app.e2e-spec.ts` +Expected: FAIL because `/api/v1/health` or `x-test-user-id` handling is missing. + +- [ ] **Step 3: Add the minimum shared runtime pieces** + +`src/common/current-user.decorator.ts` + +```ts +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const CurrentUser = createParamDecorator( + (_data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.currentUser as { id: bigint }; + }, +); +``` + +`src/common/test-user.guard.ts` + +```ts +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +@Injectable() +export class TestUserGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const rawUserId = request.headers['x-test-user-id'] ?? '1'; + request.currentUser = { id: BigInt(rawUserId) }; + return true; + } +} +``` + +`src/prisma/prisma.service.ts` + +```ts +import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on('beforeExit', async () => { + await app.close(); + }); + } +} +``` + +- [ ] **Step 4: Wire AppModule to use the guard, Prisma, and versioned health route** + +```ts +@Controller('api/v1/health') +@UseGuards(TestUserGuard) +class HealthController { + @Get() + health(@CurrentUser() user: { id: bigint }) { + return { ok: true, userId: user.id.toString() }; + } +} + +@Module({ + imports: [PrismaModule], + controllers: [HealthController], + providers: [{ provide: APP_GUARD, useClass: TestUserGuard }], +}) +export class AppModule {} +``` + +- [ ] **Step 5: Bring the Prisma schema and seed up to MVP baseline** + +Use the existing models, then add the missing exposure table and uniqueness needed by later tasks: + +```prisma +model CardExposure { + id BigInt @id @default(autoincrement()) + userId BigInt + cardId BigInt + sessionId String + positionIndex Int? + exposedAt DateTime @default(now()) + + @@index([userId, exposedAt]) + @@index([sessionId]) +} + +model CardSwipe { + // ... + @@unique([userId, cardId]) +} +``` + +Seed at least: + +```ts +await prisma.user.createMany({ + data: [ + { id: 1n, nickname: 'SOFIA', gender: 'female', ageRange: 'gen-z', relationshipStatus: 'single', onboardingCompleted: true, avatarUrl: 'https://example.com/sofia.jpg', signature: '先理解,再表达。', mbti: 'INFJ' }, + { id: 2n, nickname: 'MARCUS', gender: 'male', ageRange: '90s', relationshipStatus: 'single', onboardingCompleted: true, avatarUrl: 'https://example.com/marcus.jpg', signature: '夜晚效率更高。', mbti: 'INTJ' }, + { id: 3n, nickname: 'ELENA', gender: 'female', ageRange: '90s', relationshipStatus: 'single', onboardingCompleted: true, avatarUrl: 'https://example.com/elena.jpg', signature: '相信温柔也有力量。', mbti: 'ENFJ' }, + { id: 4n, nickname: 'JULI', gender: 'female', ageRange: 'gen-z', relationshipStatus: 'single', onboardingCompleted: true, avatarUrl: 'https://example.com/juli.jpg', signature: '喜欢旅行和探索。', mbti: 'ENFP' }, + ], + skipDuplicates: true, +}); + +await prisma.userProfileTag.createMany({ + data: [ + { userId: 1n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '价值观', source: 'onboarding' }, + { userId: 2n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '内心世界', source: 'behavior' }, + { userId: 3n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '价值观', source: 'behavior' }, + { userId: 4n, tagType: 'topic_preference', tagKey: 'topic', tagValue: '旅行与探索', source: 'onboarding' }, + ], + skipDuplicates: true, +}); + +await prisma.card.createMany({ + data: [ + { id: 101n, sourceType: 'platform', category: '社会观察', content: '坚定地认为《虎胆龙威》是一部圣诞电影。', status: 'active', agreeCount: 65, disagreeCount: 35 }, + { id: 202n, sourceType: 'platform', category: '内心世界', content: '认为晚上的效率永远比白天高。', status: 'active', agreeCount: 42, disagreeCount: 58 }, + { id: 303n, sourceType: 'platform', category: '价值观', content: '真正成熟的人会先理解,再表达自己。', status: 'active', agreeCount: 71, disagreeCount: 29 }, + { id: 404n, sourceType: 'platform', category: '旅行与探索', content: '旅行的意义是重新看见熟悉的自己。', status: 'active', agreeCount: 54, disagreeCount: 46 }, + { id: 505n, sourceType: 'platform', category: '内心世界', content: '树洞比热闹更能让人放松。', status: 'active', agreeCount: 60, disagreeCount: 40 }, + ], + skipDuplicates: true, +}); + +await prisma.discoveryPost.createMany({ + data: [ + { id: 501n, authorUserId: 1n, postType: 'featured', category: '价值观', title: '关于成熟', content: '我更认同先理解后表达。', status: 'published' }, + { id: 502n, authorUserId: 2n, postType: 'timeline', category: '内心世界', content: '今晚也想把话说给风听。', status: 'published' }, + { id: 503n, authorUserId: 4n, postType: 'timeline', category: '旅行与探索', content: '想去海边待三天不看手机。', status: 'published' }, + ], + skipDuplicates: true, +}); +``` + +- [ ] **Step 6: Generate Prisma client, run migration, reseed, and rerun the smoke test** + +Run: `npm run prisma:generate` +Expected: Prisma client generated successfully. + +Run: `npm run prisma:migrate -- --name mvp_baseline` +Expected: A migration is created and applied to the local PostgreSQL database. + +Run: `npm run prisma:seed` +Expected: Seed completes without duplicate-key errors. + +Run: `npm test -- --runInBand test/app.e2e-spec.ts` +Expected: PASS. + +- [ ] **Step 7: Commit the shared baseline** + +```bash +git add prisma src test README.md +git commit -m "feat: add backend mvp shared baseline" +``` + +### Task 2: Implement onboarding submission and profile-tag initialization + +**Files:** +- Create: `.worktrees/backend-p0/backend/src/onboarding/onboarding.module.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/onboarding.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/onboarding.service.ts` +- Create: `.worktrees/backend-p0/backend/src/onboarding/dto/submit-onboarding.dto.ts` +- Modify: `.worktrees/backend-p0/backend/src/app.module.ts` +- Test: `.worktrees/backend-p0/backend/test/onboarding.e2e-spec.ts` + +- [ ] **Step 1: Write the failing onboarding e2e spec first** + +```ts +it('submits answers, upserts tags, and marks onboarding complete', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/onboarding/submit') + .set('x-test-user-id', '1') + .send({ + answers: [ + { questionId: 1, selected: 'female' }, + { questionId: 2, selected: 'gen-z' }, + { questionId: 3, selected: 'single' }, + ], + }) + .expect(201); + + expect(response.body.data).toEqual({ onboardingCompleted: true }); +}); +``` + +- [ ] **Step 2: Run the onboarding spec to verify the endpoint is not implemented yet** + +Run: `npm test -- --runInBand test/onboarding.e2e-spec.ts` +Expected: FAIL with 404 or missing module errors. + +- [ ] **Step 3: Define the request DTO and controller contract** + +`src/onboarding/dto/submit-onboarding.dto.ts` + +```ts +class OnboardingAnswerDto { + @IsInt() + questionId: number; + + @IsOptional() + @IsString() + selected?: string; +} + +export class SubmitOnboardingDto { + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => OnboardingAnswerDto) + answers: OnboardingAnswerDto[]; +} +``` + +`src/onboarding/onboarding.controller.ts` + +```ts +@Controller('api/v1/onboarding') +export class OnboardingController { + constructor(private readonly onboardingService: OnboardingService) {} + + @Post('submit') + submit(@CurrentUser() user: { id: bigint }, @Body() dto: SubmitOnboardingDto) { + return this.onboardingService.submit(user.id, dto); + } +} +``` + +- [ ] **Step 4: Implement minimal service logic with explicit question mapping** + +```ts +const QUESTION_TAG_MAP: Record = { + 1: { tagType: 'demographic', tagKey: 'gender' }, + 2: { tagType: 'demographic', tagKey: 'age_range' }, + 3: { tagType: 'demographic', tagKey: 'relationship_status' }, +}; + +await this.prisma.$transaction(async (tx) => { + for (const answer of dto.answers) { + await tx.onboardingAnswer.upsert({ + where: { userId_questionId: { userId, questionId: answer.questionId } }, + create: { userId, questionId: answer.questionId, selectedValue: answer.selected ?? null, isSkipped: !answer.selected }, + update: { selectedValue: answer.selected ?? null, isSkipped: !answer.selected, submittedAt: new Date() }, + }); + } + + await tx.user.update({ + where: { id: userId }, + data: { + onboardingCompleted: true, + gender: dto.answers.find((item) => item.questionId === 1)?.selected ?? null, + ageRange: dto.answers.find((item) => item.questionId === 2)?.selected ?? null, + relationshipStatus: dto.answers.find((item) => item.questionId === 3)?.selected ?? null, + }, + }); +}); +``` + +- [ ] **Step 5: Add the module to AppModule and rerun the onboarding spec** + +Run: `npm test -- --runInBand test/onboarding.e2e-spec.ts` +Expected: PASS with `onboardingCompleted: true`. + +- [ ] **Step 6: Add a second test for re-submit overwrite behavior so the ambiguity is fixed in code** + +```ts +it('overwrites existing answers on repeat submit', async () => { + await request(app.getHttpServer()) + .post('/api/v1/onboarding/submit') + .set('x-test-user-id', '1') + .send({ answers: [{ questionId: 2, selected: '90s' }] }) + .expect(201); +}); +``` + +Run: `npm test -- --runInBand test/onboarding.e2e-spec.ts` +Expected: PASS, and the stored age range becomes `90s`. + +- [ ] **Step 7: Commit onboarding** + +```bash +git add src/onboarding src/app.module.ts test/onboarding.e2e-spec.ts +git commit -m "feat: implement onboarding submission" +``` + +### Task 3: Implement cards recommendation, exposure tracking, and swipe recording + +**Files:** +- Create: `.worktrees/backend-p0/backend/src/cards/cards.module.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/cards.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/cards.service.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/dto/recommend-cards.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/cards/dto/swipe-card.dto.ts` +- Modify: `.worktrees/backend-p0/backend/src/app.module.ts` +- Test: `.worktrees/backend-p0/backend/test/cards.e2e-spec.ts` + +- [ ] **Step 1: Write failing tests for recommend and swipe** + +```ts +it('returns recommend cards in frontend-compatible shape', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/cards/recommend?limit=2&category=价值观') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.items[0]).toMatchObject({ + cardId: expect.any(String), + content: expect.any(String), + user: { userId: expect.any(String), name: expect.any(String), avatar: expect.any(String) }, + stats: { agreePercent: expect.any(Number), agreeAvatars: expect.any(Array) }, + }); +}); + +it('records agree/disagree/skip and returns sessionSwipeCount', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ cardId: '101', action: 'agree', sessionId: 'session-1', sourceTab: '全部' }) + .expect(201); + + expect(response.body.data).toEqual({ recorded: true, sessionSwipeCount: 1 }); +}); +``` + +- [ ] **Step 2: Run the cards spec to lock in the failing state** + +Run: `npm test -- --runInBand test/cards.e2e-spec.ts` +Expected: FAIL because the cards module does not exist yet. + +- [ ] **Step 3: Define DTOs with the exact allowed request values** + +```ts +export class RecommendCardsDto { + @IsOptional() @Type(() => Number) @Min(1) @Max(20) limit = 1; + @IsOptional() @IsString() cursor?: string; + @IsOptional() @IsString() category?: string; + @IsOptional() @IsString() sessionId?: string; +} + +export class SwipeCardDto { + @IsString() cardId: string; + @IsIn(['agree', 'disagree', 'skip']) action: 'agree' | 'disagree' | 'skip'; + @IsString() sessionId: string; + @IsOptional() @IsString() sourceTab?: string; +} +``` + +- [ ] **Step 4: Implement recommendation with simple rules that match the spec** + +```ts +const cards = await this.prisma.card.findMany({ + where: { + status: 'active', + category: dto.category && dto.category !== '全部' ? dto.category : undefined, + swipes: { none: { userId } }, + }, + include: { author: true }, + orderBy: [{ agreeCount: 'desc' }, { createdAt: 'desc' }], + take: dto.limit, +}); + +await this.prisma.cardExposure.createMany({ + data: cards.map((card, index) => ({ + userId, + cardId: card.id, + sessionId: dto.sessionId ?? 'default-session', + positionIndex: index, + })), +}); +``` + +- [ ] **Step 5: Implement swipe recording and session aggregation** + +```ts +await this.prisma.$transaction(async (tx) => { + await tx.cardSwipe.upsert({ + where: { userId_cardId: { userId, cardId } }, + create: { userId, cardId, action: dto.action, sessionId: dto.sessionId, sourceTab: dto.sourceTab }, + update: { action: dto.action, sessionId: dto.sessionId, sourceTab: dto.sourceTab, swipedAt: new Date() }, + }); + + await tx.card.update({ + where: { id: cardId }, + data: dto.action === 'agree' + ? { agreeCount: { increment: 1 } } + : dto.action === 'disagree' + ? { disagreeCount: { increment: 1 } } + : { skipCount: { increment: 1 } }, + }); + + await tx.userSession.upsert({ + where: { sessionId: dto.sessionId }, + create: { + sessionId: dto.sessionId, + userId, + entryPage: 'home', + validSwipeCount: dto.action === 'skip' ? 0 : 1, + skipCount: dto.action === 'skip' ? 1 : 0, + }, + update: { + lastActiveAt: new Date(), + validSwipeCount: dto.action === 'skip' ? undefined : { increment: 1 }, + skipCount: dto.action === 'skip' ? { increment: 1 } : undefined, + }, + }); +}); +``` + +- [ ] **Step 6: Rerun cards e2e and add one skip-specific regression test** + +Run: `npm test -- --runInBand test/cards.e2e-spec.ts` +Expected: PASS. + +Add: + +```ts +it('does not increment validSwipeCount for skip', async () => { + await request(app.getHttpServer()) + .post('/api/v1/cards/swipe') + .set('x-test-user-id', '1') + .send({ cardId: '202', action: 'skip', sessionId: 'session-1' }) + .expect(201); +}); +``` + +Run: `npm test -- --runInBand test/cards.e2e-spec.ts` +Expected: PASS, and the session keeps valid swipes unchanged. + +- [ ] **Step 7: Commit cards** + +```bash +git add src/cards src/app.module.ts test/cards.e2e-spec.ts prisma/schema.prisma +git commit -m "feat: implement cards recommend and swipe" +``` + +### Task 4: Implement matching trigger-check with explainable rules + +**Files:** +- Create: `.worktrees/backend-p0/backend/src/matching/matching.module.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/matching.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/matching.service.ts` +- Create: `.worktrees/backend-p0/backend/src/matching/dto/trigger-check.dto.ts` +- Modify: `.worktrees/backend-p0/backend/src/app.module.ts` +- Test: `.worktrees/backend-p0/backend/test/matching.e2e-spec.ts` + +- [ ] **Step 1: Write the failing trigger-check test** + +```ts +it('returns a match candidate once session thresholds are met', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-1', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + expect(response.body.data).toMatchObject({ + shouldTrigger: expect.any(Boolean), + }); +}); +``` + +- [ ] **Step 2: Run the matching spec to confirm the route is missing** + +Run: `npm test -- --runInBand test/matching.e2e-spec.ts` +Expected: FAIL with 404. + +- [ ] **Step 3: Add a DTO that makes the trigger inputs explicit** + +```ts +export class TriggerCheckDto { + @IsString() sessionId: string; + @Type(() => Number) @Min(0) sessionSwipeCount: number; + @Type(() => Number) @Min(0) sessionDuration: number; +} +``` + +- [ ] **Step 4: Implement the simplest acceptable rules from the spec** + +```ts +const eligible = dto.sessionSwipeCount >= 3 && dto.sessionDuration >= 30; +if (!eligible) { + return { shouldTrigger: false, reason: 'threshold_not_met' }; +} + +const candidates = await this.prisma.user.findMany({ + where: { + id: { not: userId }, + onboardingCompleted: true, + status: 'active', + }, + include: { profileTags: true }, + take: 10, +}); +``` + +- [ ] **Step 5: Add explainable scoring and persist the result** + +```ts +const best = candidates + .map((candidate) => { + const candidateTopics = candidate.profileTags + .filter((tag) => tag.tagType === 'topic_preference') + .map((tag) => tag.tagValue); + const sharedTopics = userTopics.filter((topic) => candidateTopics.includes(topic)); + const score = sharedTopics.length * 25 + (candidate.avatarUrl ? 10 : 0) + 5; + const reason = sharedTopics.length > 0 + ? `你们都更关注${sharedTopics[0]}话题` + : '你们最近的活跃与表达节奏接近'; + + return { candidate, score, reason }; + }) + .sort((a, b) => b.score - a.score)[0]; + +await this.prisma.matchEvent.create({ + data: { + userId, + candidateUserId: best?.candidate.id, + sessionId: dto.sessionId, + triggerReason: best?.reason ?? 'no_candidate', + matchScore: best?.score ?? null, + resultStatus: best ? 'matched' : 'no_match', + }, +}); +``` + +- [ ] **Step 6: Add the one-shot session protection and rerun tests** + +```ts +await this.prisma.userSession.update({ + where: { sessionId: dto.sessionId }, + data: { blindBoxChecked: true, blindBoxTriggered: !!best }, +}); +``` + +Run: `npm test -- --runInBand test/matching.e2e-spec.ts` +Expected: PASS. + +Add a second test: + +```ts +it('does not trigger twice for the same session', async () => { + await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-2', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + const second = await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-2', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + expect(second.body.data).toMatchObject({ shouldTrigger: false }); +}); +``` + +Run: `npm test -- --runInBand test/matching.e2e-spec.ts` +Expected: PASS. + +- [ ] **Step 7: Commit matching** + +```bash +git add src/matching src/app.module.ts test/matching.e2e-spec.ts +git commit -m "feat: implement matching trigger check" +``` + +### Task 5: Implement discovery feed, publish, and profile aggregation + +**Files:** +- Create: `.worktrees/backend-p0/backend/src/discovery/discovery.module.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/discovery.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/discovery.service.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/dto/get-feed.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/discovery/dto/publish-post.dto.ts` +- Create: `.worktrees/backend-p0/backend/src/profile/profile.module.ts` +- Create: `.worktrees/backend-p0/backend/src/profile/profile.controller.ts` +- Create: `.worktrees/backend-p0/backend/src/profile/profile.service.ts` +- Modify: `.worktrees/backend-p0/backend/src/app.module.ts` +- Test: `.worktrees/backend-p0/backend/test/discovery.e2e-spec.ts` +- Test: `.worktrees/backend-p0/backend/test/profile.e2e-spec.ts` + +- [ ] **Step 1: Write failing discovery and profile e2e coverage** + +```ts +it('returns feed rows filtered by tab and feed type', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/discovery/feed?tabType=价值观&feedType=featured') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data.items).toEqual(expect.any(Array)); +}); + +it('returns profile info in the page-ready shape', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/profile/info') + .set('x-test-user-id', '1') + .expect(200); + + expect(response.body.data).toMatchObject({ + nickname: expect.any(String), + counts: { interactions: expect.any(Number) }, + photos: expect.any(Array), + }); +}); +``` + +- [ ] **Step 2: Run both specs to capture the red state** + +Run: `npm test -- --runInBand test/discovery.e2e-spec.ts test/profile.e2e-spec.ts` +Expected: FAIL because both modules are missing. + +- [ ] **Step 3: Implement feed query DTOs and publish DTO** + +```ts +export class GetFeedDto { + @IsOptional() @IsString() tabType?: string; + @IsOptional() @IsIn(['featured', 'timeline']) feedType?: 'featured' | 'timeline'; + @IsOptional() @IsString() cursor?: string; +} + +export class PublishPostDto { + @IsString() content: string; + @IsString() tabType: string; + @IsOptional() @IsBoolean() anonymous = false; +} +``` + +- [ ] **Step 4: Implement discovery feed and publish with MVP-safe defaults** + +```ts +const items = await this.prisma.discoveryPost.findMany({ + where: { + status: 'published', + category: dto.tabType && dto.tabType !== '全部' ? dto.tabType : undefined, + postType: dto.feedType, + }, + include: { author: true }, + orderBy: [{ createdAt: 'desc' }], + take: 10, +}); + +const created = await this.prisma.discoveryPost.create({ + data: { + authorUserId: userId, + postType: 'timeline', + category: dto.tabType, + content: dto.content, + anonymous: dto.anonymous ?? false, + status: 'published', + }, +}); +``` + +- [ ] **Step 5: Implement profile aggregation using real counts instead of page-local mock data** + +```ts +const [user, photos, swipeCount] = await Promise.all([ + this.prisma.user.findUniqueOrThrow({ where: { id: userId } }), + this.prisma.userPhoto.findMany({ where: { userId, status: 'active' }, orderBy: { sortOrder: 'asc' } }), + this.prisma.cardSwipe.count({ where: { userId } }), +]); + +return { + userId: user.id.toString(), + nickname: user.nickname, + gender: user.gender, + age: user.ageRange, + mbti: user.mbti, + signature: user.signature, + photos: photos.map((item) => item.photoUrl), + counts: { + visitors: 0, + followers: 0, + following: 0, + interactions: swipeCount, + }, +}; +``` + +- [ ] **Step 6: Add one publish e2e and rerun both suites** + +```ts +it('publishes a discovery post', async () => { + await request(app.getHttpServer()) + .post('/api/v1/discovery/publish') + .set('x-test-user-id', '1') + .send({ content: '观点文本', tabType: '价值观', anonymous: false }) + .expect(201); +}); +``` + +Run: `npm test -- --runInBand test/discovery.e2e-spec.ts test/profile.e2e-spec.ts` +Expected: PASS. + +- [ ] **Step 7: Commit discovery and profile** + +```bash +git add src/discovery src/profile src/app.module.ts test/discovery.e2e-spec.ts test/profile.e2e-spec.ts +git commit -m "feat: implement discovery and profile apis" +``` + +### Task 6: Wire the four Mini Program pages to the real backend + +**Files:** +- Modify: `app.js` +- Create: `utils/request.js` +- Create: `utils/session.js` +- Modify: `pages/onboarding/onboarding.js` +- Modify: `pages/home/home.js` +- Modify: `pages/discovery/discovery.js` +- Modify: `pages/profile/profile.js` + +- [ ] **Step 1: Create a tiny shared request helper and session helper** + +`utils/request.js` + +```js +const BASE_URL = 'http://127.0.0.1:3000'; + +function request({ url, method = 'GET', data }) { + return new Promise((resolve, reject) => { + wx.request({ + url: `${BASE_URL}${url}`, + method, + data, + header: { 'x-test-user-id': '1' }, + success: (res) => resolve(res.data.data), + fail: reject, + }); + }); +} + +module.exports = { request }; +``` + +`utils/session.js` + +```js +function ensureSessionId() { + const existing = wx.getStorageSync('swSessionId'); + if (existing) return existing; + const next = `session-${Date.now()}`; + wx.setStorageSync('swSessionId', next); + return next; +} + +module.exports = { ensureSessionId }; +``` + +- [ ] **Step 2: Replace onboarding local-only completion with submit API** + +```js +const { request } = require('../../utils/request'); + +async finishOnboarding() { + const answers = this.data.answers; + await request({ + url: '/api/v1/onboarding/submit', + method: 'POST', + data: { answers }, + }); + wx.redirectTo({ url: '/pages/home/home' }); +} +``` + +- [ ] **Step 3: Replace home page mock loop with API-driven current/next card state** + +```js +async loadNextCard() { + const sessionId = ensureSessionId(); + const data = await request({ + url: `/api/v1/cards/recommend?limit=2&category=${encodeURIComponent(this.data.currentTab)}&sessionId=${sessionId}`, + }); + + this.setData({ + currentCard: normalizeCard(data.items[0]), + nextCard: normalizeCard(data.items[1] || data.items[0]), + }); +} +``` + +- [ ] **Step 4: Replace swipe reporting and blind-box check with real calls** + +```js +await request({ + url: '/api/v1/cards/swipe', + method: 'POST', + data: { + cardId: String(this.data.currentCard.id), + action: mapDirectionToAction(direction), + sessionId: ensureSessionId(), + sourceTab: this.data.currentTab, + }, +}); + +const blindBox = await request({ + url: '/api/v1/matching/trigger-check', + method: 'POST', + data: { + sessionId: ensureSessionId(), + sessionSwipeCount: this.data.swipeSessionCount, + sessionDuration: Math.floor((Date.now() - this.data.entryTime) / 1000), + }, +}); +``` + +- [ ] **Step 5: Replace discovery and profile mocks with feed/profile requests** + +```js +const feed = await request({ + url: `/api/v1/discovery/feed?tabType=${encodeURIComponent(this.data.currentTab)}&feedType=featured`, +}); + +const profile = await request({ url: '/api/v1/profile/info' }); +this.setData({ profile }); +``` + +- [ ] **Step 6: Manually run the Mini Program against the local backend** + +Run backend: `npm run start:dev` +Expected: Nest starts on port 3000 without module import errors. + +Run Mini Program in WeChat DevTools and verify: +- onboarding submits and redirects +- home loads non-mock cards +- swipe triggers network calls +- discovery feed loads rows +- profile renders backend data + +- [ ] **Step 7: Commit frontend integration** + +```bash +git add app.js utils pages/onboarding/onboarding.js pages/home/home.js pages/discovery/discovery.js pages/profile/profile.js +git commit -m "feat: connect mini program pages to backend mvp" +``` + +### Task 7: Final verification, startup docs, and handoff + +**Files:** +- Modify: `.worktrees/backend-p0/backend/README.md` +- Modify: `README.md` + +- [ ] **Step 1: Add a backend README section that exactly matches the local run flow** + +```md +1. `docker compose up -d` +2. `cp .env.example .env` +3. `npm install` +4. `npm run prisma:generate` +5. `npm run prisma:migrate -- --name mvp_baseline` +6. `npm run prisma:seed` +7. `npm run start:dev` +``` + +- [ ] **Step 2: Add one root README note pointing frontend developers to the backend worktree** + +```md +Backend MVP lives in `.worktrees/backend-p0/backend`. Start that service before opening the Mini Program pages that now call real APIs. +``` + +- [ ] **Step 3: Run the full backend test suite** + +Run: `npm test -- --runInBand` +Expected: All `app`, `onboarding`, `cards`, `matching`, `discovery`, and `profile` e2e specs PASS. + +- [ ] **Step 4: Run a production build** + +Run: `npm run build` +Expected: Nest build completes without TypeScript errors. + +- [ ] **Step 5: Reseed once more from a clean database and smoke-test the app manually** + +Run: `npm run prisma:seed` +Expected: Seed remains idempotent. + +Then re-open Mini Program DevTools and verify the four page flows once more. + +- [ ] **Step 6: Capture final status in git** + +Run: `git status --short` +Expected: only intentional backend/frontend/doc changes remain. + +- [ ] **Step 7: Commit the release-ready MVP slice** + +```bash +git add README.md .worktrees/backend-p0/backend/README.md +git commit -m "docs: finalize backend mvp local runbook" +``` + +--- + +## Self-review + +### Spec coverage + +- Onboarding submit: covered in Task 2. +- Cards recommend + swipe + session stats: covered in Task 3. +- Matching trigger check: covered in Task 4. +- Discovery feed + publish: covered in Task 5. +- Profile info: covered in Task 5. +- PostgreSQL schema, Prisma client, seed, test auth, e2e, startup docs: covered in Tasks 1 and 7. +- Four frontend pages integration: covered in Task 6. + +### Placeholder scan + +- No `TODO`, `TBD`, or “handle appropriately” placeholders remain. +- The one spec ambiguity around trigger-check route is fixed explicitly at the top of this plan. + +### Type consistency + +- Cards use `agree | disagree | skip` consistently across schema, DTOs, and frontend mapping. +- Matching uses `sessionId`, `sessionSwipeCount`, and `sessionDuration` consistently. +- Frontend integration assumes the response wrapper shape `data.data` from the existing interceptor. diff --git a/docs/superpowers/specs/2026-04-08-backend-mvp-design.md b/docs/superpowers/specs/2026-04-08-backend-mvp-design.md new file mode 100644 index 0000000..e413dc1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-08-backend-mvp-design.md @@ -0,0 +1,1494 @@ +# SW(Same Wavelength / 同频)后端需求与架构设计文档(MVP / P0 / P1) + +> 2026-04-09 确认范围补充:本轮实现按方案 B 执行——完成完整 MVP 后端(onboarding / cards / matching / discovery / profile 全部接口、数据库、seed、e2e),前端仅接入 onboarding / home / discovery / profile 这 4 个页面;chat、微信登录、生产级推荐与审核后台不纳入本轮交付。 + +## 1. 文档目标 + +本文档用于给当前微信小程序提供一份**本地优先、可逐步演进**的后端设计基线,帮助后端开发和产品设计明确以下问题: + +- 当前产品主流程是什么 +- MVP 阶段最小必要业务对象有哪些 +- 后端数据应该如何存储 +- 当前是否必须使用微信云服务 +- PostgreSQL 是否适合作为主数据库 +- 用户画像应该如何设计 +- 匹配对象的筛选规则如何设计 +- MVP、P0、P1 以及最终形态分别做什么 + +本文档默认技术方向采用: + +> **本地优先:Node.js / NestJS(或类似分层框架) + PostgreSQL,后续再接微信能力。** + +### 1.1 本轮确认的交付边界(2026-04-09) + +本轮不是只做后端骨架,而是要交付一个可联调的 MVP 版本,范围明确如下: + +- **后端必须完成**: + - `POST /api/v1/onboarding/submit` + - `GET /api/v1/cards/recommend` + - `POST /api/v1/cards/swipe` + - `GET /api/v1/matching/trigger-check` + - `GET /api/v1/discovery/feed` + - `POST /api/v1/discovery/publish` + - `GET /api/v1/profile/info` +- **基础设施必须完成**:PostgreSQL schema、Prisma client、seed 数据、测试用户鉴权、核心 e2e 测试、启动文档 +- **前端只接入 4 个页面**: + - `pages/onboarding/onboarding.js` + - `pages/home/home.js` + - `pages/discovery/discovery.js` + - `pages/profile/profile.js` +- **本轮不做**:chat、微信登录、审核后台、生产级推荐系统、云开发迁移 + +因此,本设计文档中的 MVP 解释,以“**后端完整可用 + 4 个前端页面完成联调**”为准。 + +--- + +## 2. 产品定位 + +SW(同频)是一个以“观点表达”为入口的轻社交微信小程序。 + +产品核心路径不是“先找人聊天”,而是: + +1. 用户先完成轻量画像初始化 +2. 用户通过滑动观点卡表达态度 +3. 系统基于行为信号做内容推荐与匹配触发 +4. 用户在发现页查看更多公共内容 +5. 关系建立后进入聊天与个人档案场景 + +一句话描述: + +> 以观点表达为入口,以同频匹配为目标,以聊天和个人档案为承接。 + +--- + +## 3. 总体结论(先回答核心问题) + +### 3.1 当前后端数据怎么存? + +当前阶段建议采用: + +- **业务主库:PostgreSQL** +- **后端服务:Node.js / NestJS** +- **本地开发:Docker 启动 PostgreSQL,API 服务本地运行** +- **对象资源(头像/图片)**:MVP 阶段可先存 URL,后续接对象存储 + +这意味着当前小程序前端只负责调用 HTTP API,真正的业务状态、用户画像、滑卡行为、匹配记录、发现页内容都由你自己的后端服务管理。 + +--- + +### 3.2 PostgreSQL 可以用吗? + +**可以,而且很适合当前项目。** + +PostgreSQL 适合本项目的原因: + +1. **关系清晰**:用户、卡片、滑动行为、内容流、匹配关系都天然适合关系型建模。 +2. **支持 JSON 字段**:对画像扩展字段、推荐解释、前端兼容结构非常友好。 +3. **本地开发成本低**:本地即可跑通,不依赖微信云环境。 +4. **便于后期演进**:后续可增加 Redis、ES、消息队列,但主数据仍可稳定放在 PostgreSQL。 +5. **适合 MVP 到中期产品**:当前数据规模、查询复杂度、统计需求都在 PostgreSQL 的舒适区间内。 + +因此,**PostgreSQL 完全可以作为当前项目的主数据库,不仅能用,而且应该优先用。** + +--- + +### 3.3 一定要使用微信云服务吗? + +**不一定,也没有必要在 MVP 一开始就绑定微信云服务。** + +微信云服务适合以下场景: + +- 希望快速接入云函数、云存储、云数据库 +- 团队对微信生态部署非常熟悉 +- 产品明确长期重度依赖微信云原生能力 + +但对于当前项目,MVP 阶段更适合采用你自己的后端服务,原因如下: + +1. **架构更通用**:以后不只是微信小程序,也能扩展到 H5 / App。 +2. **本地联调更直接**:前端 + 本地 API + 本地 PostgreSQL,调试路径清晰。 +3. **业务模型更复杂**:画像、匹配、推荐、内容流、聊天,这些都更适合自主后端掌控。 +4. **后期迁移成本更低**:先把领域模型和接口设计稳住,再按需接微信登录、订阅消息等能力。 + +因此本文档结论是: + +> **MVP 阶段不要求使用微信云数据库或微信云开发。可以完全采用自建后端 + PostgreSQL。** + +后续建议接入微信能力的部分包括: + +- 微信登录态绑定(openid / unionid) +- 微信手机号授权 +- 模板消息 / 订阅消息 +- 小程序码 / 分享链路 + +--- + +## 4. 总体技术架构(本地优先) + +### 4.1 架构分层 + +建议采用以下分层: + +1. **客户端层**:微信小程序 +2. **API 层**:Node.js / NestJS Controller +3. **应用服务层**:Onboarding / Card / Match / Discovery / Profile Service +4. **领域与规则层**:画像计算、盲盒触发、候选筛选、推荐排序 +5. **数据访问层**:Repository / ORM +6. **存储层**:PostgreSQL + +可抽象为: + +```text +WeChat Mini Program + ↓ HTTPS +Node/NestJS API Service + ↓ +Application Services + Domain Rules + ↓ +PostgreSQL +``` + +--- + +### 4.2 推荐的模块拆分 + +后端建议至少拆成以下模块: + +- `auth`:登录态、游客态、后续微信授权绑定 +- `users`:用户主体信息 +- `onboarding`:引导问卷与初始画像写入 +- `cards`:观点卡读取、曝光、滑动行为 +- `matching`:盲盒触发、候选人筛选、匹配记录 +- `discovery`:内容流、内容发布 +- `profile`:个人档案聚合输出 +- `analytics`:行为埋点、统计聚合(MVP 可轻量) + +### 4.4 本轮实现策略(方案 B) + +本轮采用“**模块边界完整保留,但业务规则先做轻量可运行版**”的策略: + +1. **保留模块化结构**:`auth / users / onboarding / cards / matching / discovery / profile / prisma / common` +2. **避免过度算法化**:推荐、盲盒、候选人筛选先用规则版实现,保证可运行与可解释 +3. **优先联调闭环**:以后端接口真实可用、前端 4 个页面可切换真实 API 为优先目标 +4. **为 P1 留出升级位**:后续可以在不破坏接口的前提下替换推荐逻辑、接入微信登录、扩展聊天 + +该策略的核心原则是: + +- 当前要解决的是“**MVP 真跑通**”,而不是“推荐系统做得多聪明” +- 当前后端必须让前端摆脱 mock 数据 +- 当前实现必须可测试、可 seed、可本地启动、可继续演进 + +--- + +### 4.3 MVP 部署建议 + +本地开发阶段建议: + +- 小程序:微信开发者工具运行 +- API:本地 `localhost` 运行 +- PostgreSQL:Docker 容器运行 +- 管理工具:DBeaver / TablePlus / pgAdmin + +一个典型本地启动方式可以是: + +```text +Mini Program DevTools -> http://localhost:3000/api/v1/* +NestJS API -> localhost:3000 +PostgreSQL -> localhost:5432 +``` + +注意:微信小程序真机调试时,后续需处理合法域名、内网穿透或测试环境部署问题;但这不影响你先在本地完成后端建模和联调。 + +--- + +## 5. 当前产品主流程 + +### 5.1 流程总览 + +MVP 阶段用户主路径如下: + +1. 首次进入小程序 +2. 完成 3 题 onboarding 引导 +3. 进入首页浏览观点卡 +4. 对卡片执行左滑 / 右滑 / 上滑 +5. 系统记录行为并在满足条件时检查盲盒触发 +6. 用户切换到 Discovery 浏览公共内容流 +7. 用户查看 Profile,未来在匹配后进入 Chat + +--- + +### 5.2 阶段 A:Onboarding(新用户引导) + +#### 功能目标 + +用户首次进入产品时,通过 3 道快速问题建立基础画像,用于后续推荐和匹配。 + +#### 当前前端行为 + +- 页面:`pages/onboarding/onboarding` +- 题目数量:3 题 +- 操作:左滑、右滑、跳过、回退 +- 最后一题完成后跳转首页 + +#### 后端职责 + +- 接收 onboarding 答案 +- 保存用户的基础标签或基础画像 +- 标记用户已完成初始化 +- 为后续推荐系统提供初始输入 + +#### 业务规则 + +- 每个用户只需完成一次初始化流程 +- 允许题目跳过,跳过题不应阻断用户进入首页 +- onboarding 提交成功后,用户状态变为“已完成引导” + +#### 成功结果 + +- 用户完成初始化画像 +- 后端可基于画像为首页生成初步推荐内容 + +--- + +### 5.3 阶段 B:Home(首页观点卡交互) + +#### 功能目标 + +首页是产品核心互动区。用户通过对观点卡片进行滑动表态,持续向系统提供偏好信号。 + +#### 当前前端行为 + +- 页面:`pages/home/home` +- 当前数据源:前端 `MOCK_CARDS` +- 操作: + - 右滑:认同 + - 左滑:不认同 + - 上滑:跳过 +- 每次滑动后切下一张卡 +- 首页同时展示观点作者、标签、认同比例、参与头像 + +#### 后端职责 + +- 返回推荐观点卡列表或当前卡片 +- 接收并记录用户对卡片的行为 +- 维护会话内有效交互次数 +- 为后续盲盒匹配触发提供依据 + +#### 业务规则 + +- `agree` 表示认同 +- `disagree` 表示不认同 +- `skip` 表示跳过 +- 上滑跳过应被记录,但应与“有效偏好行为”区分统计 +- 同一张卡片的曝光与滑动行为应具备可追踪性 + +#### 成功结果 + +- 后端持续积累用户兴趣和价值观偏好信号 +- 用户可无感知地持续浏览下一张观点卡 + +--- + +### 5.4 阶段 C:Blind Box Trigger(盲盒触发) + +#### 功能目标 + +在用户完成一定量有效互动后,为其提供一次可能的“同频匹配”触发机会,作为互动奖励。 + +#### 当前前端行为 + +- 条件示意: + - 本次会话滑动次数 >= 3 + - 在线时长 > 30 秒 +- 满足条件后调用盲盒触发检查接口 + +#### 后端职责 + +- 判断当前会话是否满足盲盒触发条件 +- 如果满足,返回一个匹配候选用户 +- 如果不满足,返回不触发结果 + +#### 业务规则 + +- 盲盒触发是奖励机制,不是主入口 +- 不要求每次条件达成都一定命中匹配对象 +- MVP 阶段匹配规则可以简单化,但必须可解释 + +#### 成功结果 + +- 用户在高参与状态下获得一次潜在“连接”机会 + +--- + +### 5.5 阶段 D:Discovery(发现 / 真心话广场) + +#### 功能目标 + +为用户提供开放内容场景,使用户除了首页推荐卡片之外,还能浏览平台公共内容流。 + +#### 当前前端行为 + +- 页面:`pages/discovery/discovery` +- 内容类型规划: + - `featured`:热门投票 / 热门观点 + - `timeline`:时间线 / 树洞内容 +- 当前只有基础页面与发布入口占位 + +#### 后端职责 + +- 提供 feed 列表接口 +- 根据 tab 和 feedType 返回不同内容 +- 接收用户发布的新观点内容 + +#### 业务规则 + +- feed 至少支持分类维度: + - 全部 + - 内心世界 + - 旅行与探索 + - 价值观 + - 社会观察 +- 发布内容必须归属某一分类 +- MVP 阶段可先不实现复杂审核流,但应保留状态字段 + +#### 成功结果 + +- 用户能从“被推荐”扩展到“主动探索公共内容” + +--- + +### 5.6 阶段 E:Profile(我的档案) + +#### 功能目标 + +展示用户画像与统计信息,作为个人身份、兴趣标签和运营承载页。 + +#### 当前前端行为 + +- 页面:`pages/profile/profile` +- 当前为静态 mock 信息 +- 文档规划包含昵称、MBTI、签名、统计数据、照片等 + +#### 后端职责 + +- 返回用户档案基本信息 +- 返回用户的展示型标签和统计数据 +- 支持前端渲染照片、签名、MBTI、交互数等 + +#### 成功结果 + +- 用户可查看“我是谁”以及“我在产品中的互动状态” + +--- + +### 5.7 阶段 F:Chat(小纸条) + +#### 功能目标 + +承接用户建立连接后的私信会话场景。 + +#### 当前状态 + +- 页面:`pages/chat/chat` +- 当前没有真实消息逻辑 +- 已具备页面入口定位 + +#### 后端定位 + +MVP 第一阶段可暂不实现完整聊天系统,但应在业务设计中保留其定位: + +- 聊天不是起点,而是关系建立后的承接场景 +- 聊天会话应来源于匹配或未来的关注 / 主动发起行为 + +--- + +## 6. 领域模型(MVP 核心) + +本节定义后端在 MVP 阶段需要关注的最小业务对象。 + +### 6.1 User + +表示平台用户的主体身份。 + +核心字段建议: + +- `userId` +- `nickname` +- `avatar` +- `gender` +- `ageRange` +- `relationshipStatus` +- `mbti`(可为空) +- `signature` +- `createdAt` +- `onboardingCompleted` + +--- + +### 6.2 OnboardingAnswer + +表示用户在引导阶段提交的答案。 + +核心字段建议: + +- `userId` +- `questionId` +- `selected` +- `submittedAt` + +用途: + +- 形成用户初始画像 +- 作为推荐和匹配的基础输入 + +--- + +### 6.3 UserProfileTag + +表示从 onboarding 或用户行为中沉淀出的标签。 + +核心字段建议: + +- `userId` +- `tagType` +- `tagValue` +- `source`(onboarding / behavior / system) +- `weight` + +用途: + +- 用于推荐排序 +- 用于匹配筛选 + +--- + +### 6.4 Card + +表示首页供用户滑动表态的观点卡。 + +核心字段建议: + +- `cardId` +- `content` +- `category` +- `authorUserId` +- `status` +- `agreeCount` +- `disagreeCount` +- `createdAt` + +用途: + +- 首页推荐内容载体 +- 用户偏好信号采集入口 + +--- + +### 6.5 SwipeAction + +表示用户对某张卡片产生的一次行为记录。 + +核心字段建议: + +- `userId` +- `cardId` +- `action`(agree / disagree / skip) +- `sessionId` +- `timestamp` + +用途: + +- 沉淀行为数据 +- 统计会话活跃度 +- 驱动推荐、盲盒和画像更新 + +--- + +### 6.6 SessionActivity + +表示用户一次进入首页后的会话级行为统计。 + +核心字段建议: + +- `sessionId` +- `userId` +- `entryTime` +- `validSwipeCount` +- `skipCount` +- `durationSeconds` +- `blindBoxChecked` + +用途: + +- 判断是否满足盲盒触发条件 +- 支撑埋点与活跃度统计 + +--- + +### 6.7 MatchCandidate + +表示系统在盲盒触发场景下返回的候选匹配对象。 + +核心字段建议: + +- `userId` +- `nickname` +- `avatar` +- `matchReason` +- `matchScore`(MVP 可选) + +用途: + +- 向前端返回匹配结果 +- 作为未来聊天关系的来源 + +--- + +### 6.8 DiscoveryFeedItem + +表示发现页内容流中的一条内容。 + +核心字段建议: + +- `feedId` +- `type`(featured / timeline) +- `category` +- `title` +- `content` +- `authorUserId` +- `stats` +- `createdAt` +- `status` + +用途: + +- 支撑发现页公共内容流 +- 支持热门观点与时间线内容展示 + +--- + +### 6.9 ProfileSnapshot + +表示用户档案展示时需要返回的聚合视图。 + +核心字段建议: + +- `userId` +- `nickname` +- `gender` +- `age` +- `mbti` +- `signature` +- `photos` +- `counts.visitors` +- `counts.followers` +- `counts.following` +- `counts.interactions` + +用途: + +- 提供 profile 页面完整展示数据 + +--- + +## 7. 领域关系说明 + +- `User` 提交多个 `OnboardingAnswer` +- `OnboardingAnswer` 与用户画像一起沉淀为 `UserProfileTag` +- `User` 浏览多个 `Card` +- `User` 对 `Card` 产生多个 `SwipeAction` +- 多个 `SwipeAction` 聚合到一个 `SessionActivity` +- `SessionActivity` 可触发一次 `MatchCandidate` 检查 +- `DiscoveryFeedItem` 可以来自平台内容池或用户发布内容 +- `ProfileSnapshot` 是 `User` 及其统计信息的聚合输出 + +--- + +## 8. 用户画像设计 + +用户画像不应只理解为“静态注册资料”,而应分成至少三层: + +1. **静态画像**:用户自己填的基础属性 +2. **行为画像**:用户在产品中的真实偏好行为 +3. **关系画像**:用户与谁更容易形成连接(P1 以后逐步增强) + +--- + +### 8.1 静态画像(MVP 必做) + +来源: + +- onboarding 三题 +- 用户主动填写的基础资料 + +建议存储维度: + +- 性别 `gender` +- 年龄段 `age_range` +- 感情状态 `relationship_status` +- 昵称 `nickname` +- 头像 `avatar_url` +- 个性签名 `signature` +- MBTI(可空)`mbti` +- 城市 / 地区(可空,P1 更有价值) + +这些字段主要用于: + +- 首页推荐的基础过滤 +- 盲盒匹配的初筛 +- Profile 展示 + +--- + +### 8.2 行为画像(MVP 必做) + +来源: + +- 卡片曝光 +- agree / disagree / skip +- tab 切换偏好 +- discovery 浏览与发布 + +建议沉淀的行为维度: + +- 分类偏好:更常停留/更常认同的主题分类 +- 观点取向:对某些态度类内容整体偏正向还是负向 +- 活跃节奏:活跃天数、会话时长、单次刷卡量 +- 参与强度:有效滑动率、跳过率、发布率 +- 内容表达倾向:更喜欢发布树洞、观点、投票型内容 + +这些字段不一定全部实时存在 `users` 表里,很多适合: + +- 原始记录落行为表 +- 聚合结果进入画像快照表 / 标签表 + +--- + +### 8.3 关系画像(P1 起增强) + +来源: + +- 匹配命中记录 +- 匹配后是否进入聊天 +- 聊天是否持续 +- 是否互相关注 / 收藏 / 再次互动 + +建议沉淀的关系维度: + +- 容易匹配到哪类人 +- 哪类人群的匹配成功率更高 +- 哪些话题更容易触发连接 +- 匹配后关系持续时长 + +这部分在 MVP 不必做复杂算法,但设计上应留口。 + +--- + +### 8.4 用户画像落库建议 + +建议不要把所有画像都塞进 `users` 一张表,而应采用: + +- `users`:主体基础字段 +- `onboarding_answers`:原始答卷 +- `user_profile_tags`:标签化结果 +- `user_profile_snapshots`:聚合画像快照(可选,MVP 也可以先不单独建) + +这样做的好处: + +- 原始数据与聚合数据分层 +- 便于后续重算画像 +- 便于调整推荐与匹配规则 + +--- + +## 9. 匹配对象筛选规则设计 + +### 9.1 设计原则 + +当前产品里的“盲盒匹配”不是即时婚恋匹配,也不是强精准推荐,而是: + +- 在高参与状态下给用户一个“值得连接的人” +- 规则必须足够轻量 +- 结果必须大致合理 +- 原因必须可解释 + +因此 MVP 阶段不要做黑盒算法,而应做**可解释、可逐步优化**的规则系统。 + +--- + +### 9.2 MVP 阶段匹配规则 + +建议使用“四段式筛选”: + +#### 第一步:基础过滤 + +过滤掉不应被匹配的人: + +- 自己本人 +- 已经在短期内匹配过的人 +- 被禁用 / 注销 / 不活跃账号 +- 未完成 onboarding 的用户 +- 不满足最基本资料条件的用户 + +#### 第二步:可连接性过滤 + +优先选择当前可产生连接的人: + +- 最近活跃用户优先 +- 最近 24 小时有浏览/滑卡/发布行为的用户优先 +- 有头像、昵称、基础资料完整的用户优先 + +#### 第三步:同频度粗排 + +MVP 可使用简单评分: + +- onboarding 标签相近:+30 +- 常认同的主题分类重合:+25 +- 最近行为取向接近:+20 +- 活跃时段接近:+10 +- 内容表达强度接近:+10 +- 随机探索因子:+5 + +最终得到一个 `match_score`,选 Top N 候选。 + +#### 第四步:结果解释生成 + +返回时带上 `matchReason`,例如: + +- “你们都更关注价值观类话题” +- “你们最近都频繁认同内心世界相关观点” +- “你们的基础标签较接近,且活跃时间段相似” + +这样前端展示会更自然,也方便产品以后优化。 + +--- + +### 9.3 P1 阶段匹配规则增强 + +P1 可以增加: + +- 更细粒度标签权重 +- 相似度向量计算 +- 匹配成功率反馈回流 +- 排除低质量候选人 +- 多样性控制(避免总是同一类人) + +--- + +### 9.4 长期目标匹配规则 + +最终可演进为: + +- 规则系统 + 统计学习的混合模式 +- 基于互动结果进行闭环优化 +- 将“匹配是否进入聊天、聊天是否持续、是否再次互动”纳入模型反馈 + +但这不属于当前 MVP 必做范围。 + +--- + +## 10. 数据表草案(PostgreSQL) + +以下为适合当前项目的首版数据表设计方向。MVP 不要求一次性全部建满,但建议结构先想清楚。 + +--- + +### 10.1 `users` + +用户主表。 + +建议字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 用户 ID | +| `wechat_openid` | varchar(64) null | 后续接微信登录 | +| `wechat_unionid` | varchar(64) null | 后续接微信生态 | +| `nickname` | varchar(64) | 昵称 | +| `avatar_url` | text null | 头像 | +| `gender` | varchar(16) null | 性别 | +| `age_range` | varchar(32) null | 年龄段 | +| `relationship_status` | varchar(32) null | 感情状态 | +| `mbti` | varchar(16) null | MBTI | +| `signature` | varchar(255) null | 个性签名 | +| `city` | varchar(64) null | 城市 | +| `onboarding_completed` | boolean default false | 是否完成引导 | +| `status` | varchar(16) default 'active' | 用户状态 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +--- + +### 10.2 `onboarding_answers` + +保存用户原始答卷。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `user_id` | bigint | 用户 ID | +| `question_id` | int | 题目 ID | +| `selected_value` | varchar(64) null | 用户选择 | +| `is_skipped` | boolean default false | 是否跳过 | +| `submitted_at` | timestamptz | 提交时间 | + +建议唯一约束: + +- `unique(user_id, question_id)` + +--- + +### 10.3 `user_profile_tags` + +用户标签表,用于画像、推荐、匹配。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `user_id` | bigint | 用户 ID | +| `tag_type` | varchar(32) | 标签类型 | +| `tag_key` | varchar(64) | 标签键 | +| `tag_value` | varchar(128) | 标签值 | +| `weight` | numeric(8,2) default 1 | 权重 | +| `source` | varchar(16) | 来源:onboarding / behavior / system | +| `updated_at` | timestamptz | 更新时间 | + +示例: + +- `tag_type=topic_preference`, `tag_value=values` +- `tag_type=viewpoint_style`, `tag_value=empathetic` + +--- + +### 10.4 `cards` + +首页观点卡主表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 卡片 ID | +| `author_user_id` | bigint null | 作者 ID,可为空表示平台卡 | +| `source_type` | varchar(16) | platform / user | +| `category` | varchar(32) | 分类 | +| `content` | text | 卡片内容 | +| `status` | varchar(16) | draft / active / hidden | +| `agree_count` | int default 0 | 认同数 | +| `disagree_count` | int default 0 | 不认同数 | +| `skip_count` | int default 0 | 跳过数 | +| `exposure_count` | int default 0 | 曝光数 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +--- + +### 10.5 `card_exposures` + +卡片曝光记录表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `user_id` | bigint | 浏览用户 | +| `card_id` | bigint | 卡片 | +| `session_id` | varchar(64) | 会话 ID | +| `position_index` | int null | 列表/队列位置 | +| `exposed_at` | timestamptz | 曝光时间 | + +说明: + +- 严格来说,推荐系统最好把“曝光”和“滑动”分开记录。 +- MVP 若希望先简化,也可以先只做 `card_swipes`,但推荐长期保留曝光表。 + +--- + +### 10.6 `card_swipes` + +卡片滑动行为表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `user_id` | bigint | 用户 | +| `card_id` | bigint | 卡片 | +| `session_id` | varchar(64) | 会话 ID | +| `action` | varchar(16) | agree / disagree / skip | +| `source_tab` | varchar(32) null | 当前 tab | +| `swiped_at` | timestamptz | 行为时间 | + +推荐索引: + +- `(user_id, swiped_at desc)` +- `(card_id, swiped_at desc)` +- `(session_id)` + +--- + +### 10.7 `user_sessions` + +用户会话统计表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `session_id` | varchar(64) unique | 会话 ID | +| `user_id` | bigint | 用户 | +| `entry_page` | varchar(32) | 入口页 | +| `entered_at` | timestamptz | 进入时间 | +| `last_active_at` | timestamptz | 最近活跃时间 | +| `valid_swipe_count` | int default 0 | 有效滑动数 | +| `skip_count` | int default 0 | 跳过数 | +| `blind_box_checked` | boolean default false | 是否已检查 | +| `blind_box_triggered` | boolean default false | 是否已触发 | + +--- + +### 10.8 `match_events` + +盲盒匹配事件表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `user_id` | bigint | 发起触发的用户 | +| `candidate_user_id` | bigint null | 候选用户 | +| `session_id` | varchar(64) | 会话 ID | +| `trigger_reason` | varchar(128) | 触发原因 | +| `match_score` | numeric(8,2) null | 匹配分 | +| `result_status` | varchar(16) | no_match / matched / expired | +| `created_at` | timestamptz | 创建时间 | + +用途: + +- 记录盲盒是否触发 +- 支撑后续匹配效果分析 + +--- + +### 10.9 `discovery_posts` + +发现页内容表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `author_user_id` | bigint | 作者 | +| `post_type` | varchar(16) | featured / timeline | +| `category` | varchar(32) | 分类 | +| `title` | varchar(255) null | 标题 | +| `content` | text | 内容 | +| `anonymous` | boolean default false | 是否匿名 | +| `status` | varchar(16) | draft / published / hidden | +| `like_count` | int default 0 | 点赞数 | +| `comment_count` | int default 0 | 评论数 | +| `created_at` | timestamptz | 创建时间 | +| `updated_at` | timestamptz | 更新时间 | + +--- + +### 10.10 `user_photos` + +用户照片表。 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | bigserial pk | 主键 | +| `user_id` | bigint | 用户 | +| `photo_url` | text | 图片地址 | +| `sort_order` | int default 0 | 排序 | +| `status` | varchar(16) default 'active' | 状态 | +| `created_at` | timestamptz | 创建时间 | + +--- + +## 11. MVP API 清单 + +以下接口为当前 MVP 最优先支持接口,且均已在现有前端文档或页面逻辑中出现。 + +### 11.1 POST `/api/v1/onboarding/submit` + +#### 用途 + +提交 onboarding 的 3 题答案,建立用户基础画像。 + +#### Request Body + +```json +{ + "answers": [ + { "questionId": 1, "selected": "female" }, + { "questionId": 2, "selected": "gen-z" }, + { "questionId": 3, "selected": "single" } + ] +} +``` + +#### Response + +```json +{ + "code": 200, + "message": "success", + "data": { + "onboardingCompleted": true + } +} +``` + +#### 业务规则 + +- 允许少量题目为空或被跳过 +- 成功提交后更新用户 onboarding 状态 +- 重复提交时以最后一次结果覆盖,或限制重复提交,MVP 需二选一并固定 + +#### 前端调用位置 + +- `pages/onboarding/onboarding.js` → `animateSwipe()` 最后一题完成时 + +--- + +### 11.2 GET `/api/v1/cards/recommend` + +#### 用途 + +获取首页推荐观点卡片。 + +#### Query Params + +- `limit` +- `cursor` +- `category`(可选,映射首页 tab) + +#### Response + +```json +{ + "code": 200, + "message": "success", + "data": { + "cursor": "next_cursor_token", + "items": [ + { + "cardId": 1001, + "content": "坚定地认为《虎胆龙威》是一部圣诞电影。", + "tags": "态度 · 幽默", + "user": { + "userId": 201, + "name": "SOFIA", + "avatar": "https://..." + }, + "stats": { + "agreePercent": 65, + "agreeAvatars": ["https://..."], + "disagreeAvatar": "https://..." + } + } + ] + } +} +``` + +#### 业务规则 + +- 优先根据用户画像和近期行为做推荐 +- MVP 阶段允许先使用简单排序策略 +- 返回结构需兼容现有前端卡片渲染字段 + +#### 前端调用位置 + +- `pages/home/home.js` → `loadNextCard()` + +--- + +### 11.3 POST `/api/v1/cards/swipe` + +#### 用途 + +上报用户对卡片的一次滑动行为。 + +#### Request Body + +```json +{ + "cardId": 1001, + "action": "agree", + "timestamp": 1743600000000 +} +``` + +#### Response + +```json +{ + "code": 200, + "message": "success", + "data": { + "recorded": true, + "sessionSwipeCount": 3 + } +} +``` + +#### 业务规则 + +- `action` 允许值:`agree` / `disagree` / `skip` +- 后端需记录行为并更新会话计数 +- `agree`、`disagree` 视为有效偏好行为;`skip` 可单独统计 + +#### 前端调用位置 + +- `pages/home/home.js` → `recordSwipe(direction)` + +--- + +### 11.4 POST `/api/v1/blind-box/trigger-check` + +#### 用途 + +检查当前会话是否应触发盲盒匹配。 + +#### Request Body + +```json +{ + "sessionSwipeCount": 3, + "sessionDuration": 45 +} +``` + +#### Response + +```json +{ + "code": 200, + "message": "success", + "data": { + "shouldTrigger": true, + "matchUser": { + "userId": 302, + "name": "MARCUS", + "avatar": "https://..." + } + } +} +``` + +#### 业务规则 + +- 后端以会话行为和时长判断是否触发 +- 即使满足阈值,也允许返回 `shouldTrigger = false` +- 当触发成功时,应避免短时间内重复触发 + +#### 前端调用位置 + +- `pages/home/home.js` → `checkBlindBoxTrigger()` + +--- + +### 11.5 GET `/api/v1/discovery/feed` + +#### 用途 + +获取发现页内容流。 + +#### Query Params + +- `tabType` +- `feedType` +- `cursor` + +#### Response + +兼容 `featured` 与 `timeline` 两种内容结构。 + +#### 业务规则 + +- 至少支持 tab 分类筛选 +- 至少支持 featured / timeline 两种流 +- MVP 阶段允许先返回 mock 风格的统一结构 + +#### 前端调用位置 + +- `pages/discovery/discovery.js` → `onLoad()` / tab 切换时 + +--- + +### 11.6 POST `/api/v1/discovery/publish` + +#### 用途 + +用户在发现页发布一条新观点或内容。 + +#### Request Body + +```json +{ + "content": "观点文本", + "tabType": "values", + "anonymous": false +} +``` + +#### Response + +```json +{ + "code": 200, + "message": "success", + "data": { + "feedId": 502, + "status": "published" + } +} +``` + +#### 业务规则 + +- 内容必须归属某一分类 +- 应保留匿名发布能力字段 +- MVP 阶段可默认直接发布,但需保留内容状态位 + +#### 前端调用位置 + +- `pages/discovery/discovery.js` → `openPublish()` + +--- + +### 11.7 GET `/api/v1/profile/info` + +#### 用途 + +获取当前用户个人档案展示信息。 + +#### Response + +```json +{ + "code": 200, + "message": "success", + "data": { + "userId": 8801, + "nickname": "林深处的麋鹿", + "gender": "female", + "age": 24, + "mbti": "INFJ-T", + "signature": "在喧嚣的世界寻找同频的声音...", + "counts": { + "visitors": 1200, + "followers": 458, + "following": 89, + "interactions": 2400 + }, + "photos": ["https://...", "https://..."] + } +} +``` + +#### 业务规则 + +- 返回 Profile 页面展示所需的聚合数据 +- 统计值允许先采用简化计算或 mock 数据回填 + +#### 前端调用位置 + +- `pages/profile/profile.js` → `onLoad()` + +--- + +## 12. 阶段路线图:MVP / P0 / P1 / 最终形态 + +这里将“当前只做 MVP”与“架构必须想清楚”结合起来描述。 + +--- + +### 12.1 MVP 版本:先打通最小主链路 + +MVP 的目标不是把社交系统全部做完,而是验证这条链路是否成立: + +> 新用户初始化画像 → 刷观点卡表达态度 → 系统记录偏好 → 在合适时机给出连接机会。 + +MVP 应完成: + +- 用户基础身份建立 +- onboarding 答卷入库 +- 首页卡片读取 +- 滑卡行为上报 +- 会话统计与盲盒触发检查 +- 发现页基础 feed +- Profile 基础信息展示 + +MVP 不做或暂缓: + +- 完整聊天系统 +- 高级推荐算法 +- 复杂关系链 +- 审核后台 +- 风控体系 + +--- + +### 12.2 P0:必须先做的后端能力 + +P0 是“后端第一阶段上线所必须具备”的能力。 + +#### P0 功能范围 + +1. 用户主表与游客态/测试态账号方案 +2. onboarding 提交接口与答卷落库 +3. 首页推荐卡接口(允许简单排序) +4. 滑卡行为记录接口 +5. 会话统计与盲盒触发接口 +6. discovery feed 查询接口 +7. profile 聚合查询接口 + +#### P0 技术范围 + +1. Node/NestJS 服务骨架 +2. PostgreSQL 建库建表 +3. 基础鉴权方案(先用测试 token / mock user) +4. 统一错误码与日志 +5. 基础数据库索引 + +#### P0 交付标准 + +- 前端可去掉主要 mock 数据 +- 核心链路可完整联调 +- 数据可真实落库 +- 能支持内部测试 + +--- + +### 12.3 P1:第一版迭代 + +P1 的目标不是重写,而是在 P0 跑通基础上,提升“质量”和“可持续性”。 + +#### P1 功能增强 + +1. 接入微信登录 / openid 绑定 +2. 用户资料编辑 +3. 更完整的 Profile 数据 +4. 发现页发布能力正式化 +5. 匹配规则增强与解释优化 +6. 基础关系链(如匹配记录、连接状态) +7. 聊天能力预留或轻量上线 + +#### P1 数据与系统增强 + +1. 画像标签自动聚合 +2. 曝光/滑动拆分统计 +3. 更稳定的推荐候选池机制 +4. 内容状态流转(published / hidden / review) +5. 基础运营字段和审计字段 + +#### P1 目标 + +- 提高推荐与匹配的合理性 +- 提高用户资料与内容系统完整度 +- 为聊天与更强社交关系打基础 + +--- + +### 12.4 最终形态(长期目标) + +最终 SW 应演进为一个围绕“观点表达与同频连接”的轻社交系统。 + +长期可能包含: + +- 更完整的个体画像系统 +- 更强的匹配策略与推荐引擎 +- 正式聊天系统 +- 关系链系统(关注、收藏、拉黑、举报) +- 内容审核与运营后台 +- 微信生态能力深度接入 +- 多端化(小程序 + H5 + App) + +最终系统形态应是: + +> 内容表达、偏好理解、关系发现、连接承接、长期互动,形成闭环。 + +--- + +## 13. 建议的后端实现优先级 + +建议后端开发顺序如下: + +1. 用户身份与 onboarding 提交 +2. 首页卡片推荐接口 +3. 滑动行为记录接口 +4. 盲盒触发检查接口 +5. Profile 信息接口 +6. Discovery feed 接口 +7. Discovery 发布接口 + +这样可以优先打通核心主链路: + +> 新用户进入 → 完成引导 → 首页刷卡 → 上报行为 → 触发匹配检查 + +--- + +## 14. MVP 范围边界 + +### 14.1 本阶段要做 + +- 用户 onboarding 数据入库 +- 首页推荐卡片读取 +- 滑动行为记录 +- 盲盒触发检查 +- 发现页 feed 获取与简单发布 +- Profile 信息读取 + +### 14.2 本阶段不做或暂缓 + +- 完整即时聊天系统 +- 复杂推荐算法 +- 完整关系链(关注、拉黑、举报) +- 内容审核后台 +- 复杂风控与反作弊 +- Premium 商业化能力 + +--- + +## 15. 验收标准(MVP 级) + +当后端完成以下目标时,可认为 MVP 核心链路可联调: + +- 用户能提交 onboarding 并成功进入首页 +- 首页能从后端获取可渲染的卡片数据 +- 用户每次滑卡都能成功上报行为 +- 后端能根据会话条件返回盲盒触发结果 +- Discovery 能成功拉取内容流 +- Profile 能成功返回档案信息 +- PostgreSQL 中能够查询到关键业务数据落库结果 + +--- + +## 16. 待确认问题(进入实现前建议定稿) + +以下问题不影响本文档成立,但建议在正式开始数据库建模和接口开发前确认: + +1. 首页卡片内容来源是平台内容池,还是允许用户发布内容进入推荐池? +2. onboarding 是否允许重复提交覆盖? +3. 盲盒触发后,是只展示一次候选,还是允许用户做接受/忽略? +4. P1 的聊天是否一定基于匹配成功后开启? +5. discovery 的 `featured` 与 `timeline` 是否最终统一为一套内容主表? +6. 画像标签更新是实时写入,还是定时聚合? + +--- + +## 17. 最终建议(给当前项目的明确结论) + +如果你现在要开始写这个小程序的后端,建议按下面这条路线直接开工: + +1. **先用 Node/NestJS + PostgreSQL 在本地搭起来** +2. **先实现 P0 的 6~7 个核心接口,不要先碰聊天系统** +3. **先把用户、答卷、卡片、滑动、会话、匹配事件、发现内容这几类表设计清楚** +4. **先做规则型匹配,不做复杂算法** +5. **微信能力放到 P1 接入,不阻塞 MVP 联调** + +也就是说,当前最合理的路线不是: + +> 先选微信云开发再说 + +而是: + +> **先把产品主链路、业务模型、数据库结构和 API 跑通;微信能力后接。** + +这条路线最符合当前项目阶段,也最利于你本地快速启动。 \ No newline at end of file diff --git a/pages/discovery/discovery.js b/pages/discovery/discovery.js index f1e6789..01de6d3 100644 --- a/pages/discovery/discovery.js +++ b/pages/discovery/discovery.js @@ -1,16 +1,40 @@ +const { request } = require('../../utils/request'); + Page({ data: { statusBarHeight: 20, windowHeight: 800, tabs: ['全部', '内心世界', '旅行与探索', '价值观', '社会观察'], currentTab: '全部', + featuredItems: [], + timelineItems: [], }, - onLoad() { + async onLoad() { const windowInfo = wx.getWindowInfo(); this.setData({ statusBarHeight: windowInfo.statusBarHeight || 44, windowHeight: windowInfo.windowHeight }); + await this.loadFeed(); + }, + async loadFeed() { + try { + const [featured, timeline] = await Promise.all([ + request({ + url: `/api/v1/discovery/feed?tabType=${encodeURIComponent(this.data.currentTab)}&feedType=featured` + }), + request({ + url: `/api/v1/discovery/feed?tabType=${encodeURIComponent(this.data.currentTab)}&feedType=timeline` + }) + ]); + + this.setData({ + featuredItems: featured.items || [], + timelineItems: timeline.items || [] + }); + } catch (error) { + wx.showToast({ title: '加载失败', icon: 'none' }); + } }, goToHome() { wx.redirectTo({ url: '/pages/home/home' }); @@ -18,7 +42,37 @@ Page({ goToChat() { wx.redirectTo({ url: '/pages/chat/chat' }); }, - openPublish() { - wx.showToast({ title: '发布', icon: 'none' }); + async openPublish() { + const result = await wx.showModal({ + title: '发布观点', + editable: true, + placeholderText: '写下你此刻的想法' + }); + + if (!result.confirm || !result.content) { + return; + } + + try { + await request({ + url: '/api/v1/discovery/publish', + method: 'POST', + data: { + content: result.content, + tabType: this.data.currentTab === '全部' ? '价值观' : this.data.currentTab, + anonymous: false + } + }); + wx.showToast({ title: '已发布', icon: 'none' }); + await this.loadFeed(); + } catch (error) { + wx.showToast({ title: '发布失败', icon: 'none' }); + } + }, + async switchTab(e) { + this.setData({ + currentTab: e.currentTarget.dataset.tab + }); + await this.loadFeed(); } }); diff --git a/pages/discovery/discovery.wxml b/pages/discovery/discovery.wxml index 3af56fb..f712540 100644 --- a/pages/discovery/discovery.wxml +++ b/pages/discovery/discovery.wxml @@ -17,11 +17,15 @@ - ALL - INTERNAL - TRAVEL - VALUES - POLITICS + + {{item}} + @@ -31,9 +35,9 @@ - + - 如果时间可以倒流,你最想回到哪一年的夏天? + {{featuredItems[0].title || featuredItems[0].content}} @@ -41,20 +45,19 @@ - 2008: 奥运与旧时光 (65%) - 2019: 最后的安稳 (35%) + {{featuredItems[0].category}} · {{featuredItems[0].author.nickname}} + {{featuredItems[0].stats.likeCount}} 喜欢 · {{featuredItems[0].stats.commentCount}} 评论 - - - +128 + + +{{featuredItems[0].stats.likeCount}} 正在共鸣中... - "那年夏天没有口罩,只有吃不完的碎碎冰和永远做不完的暑假作业..." + "{{featuredItems[0].content}}" @@ -73,16 +76,10 @@ - - - 12 MINS AGO · 匿名猫 - 在大城市待久了,有时候真的分不清是在生活,还是只是在维持生存的体征。今天下班路上的晚霞好美,突然好想哭。 - - - - - 45 MINS AGO · 荒原狼 - 辞职后的第30天。没有焦虑,只有一种如释重负的空虚感。我开始练习每天只做一个决定:中午吃什么。 + + + {{item.author.nickname}} · {{item.category}} + {{item.content}} diff --git a/pages/home/home.js b/pages/home/home.js index a16eefd..b37bef9 100644 --- a/pages/home/home.js +++ b/pages/home/home.js @@ -1,4 +1,7 @@ // pages/home/home.js +const { request } = require('../../utils/request'); +const { ensureSessionId } = require('../../utils/session'); + const MOCK_CARDS = [ { id: 101, @@ -50,6 +53,21 @@ function cloneCard(card) { return JSON.parse(JSON.stringify(card)); } +function normalizeCard(card) { + return { + id: Number(card.cardId), + user: { + name: card.user.name, + avatar: card.user.avatar + }, + tags: Array.isArray(card.tags) ? card.tags : String(card.tags || '').split(' · ').filter(Boolean), + content: card.content, + agreePercent: card.stats?.agreePercent || 0, + agreeAvatars: card.stats?.agreeAvatars || [], + disagreeAvatar: card.stats?.disagreeAvatar || '' + }; +} + function getNextMockCard(currentId) { const currentIndex = MOCK_CARDS.findIndex((card) => card.id === currentId); const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % MOCK_CARDS.length; @@ -107,6 +125,8 @@ Page({ headerContentMT: menuBtn.top - (windowInfo.statusBarHeight || 44), entryTime: Date.now() }); + ensureSessionId(); + this.loadNextCard(); }, onShareAppMessage() { @@ -232,43 +252,100 @@ Page({ }, /* ====== Business Logic ====== */ - recordSwipe(direction) { - // API Call to /api/v1/cards/swipe expected here + async recordSwipe(direction) { + const actionMap = { + right: 'agree', + left: 'disagree', + up: 'skip' + }; + + try { + const result = await request({ + url: '/api/v1/cards/swipe', + method: 'POST', + data: { + cardId: String(this.data.currentCard.id), + action: actionMap[direction], + sessionId: ensureSessionId(), + sourceTab: this.data.currentTab + } + }); - // Add to session count - let sc = this.data.swipeSessionCount + 1; - this.setData({ swipeSessionCount: sc }); + this.setData({ swipeSessionCount: result.sessionSwipeCount }); - // Check blindbox trigger (e.g., 3 valid swipes + 5 mins, simulating fast for demo) - const timeSpent = (Date.now() - this.data.entryTime) / 1000; - if (sc >= 3 && timeSpent > 30) { - this.checkBlindBoxTrigger(); + const timeSpent = Math.floor((Date.now() - this.data.entryTime) / 1000); + if (result.sessionSwipeCount >= 3 && timeSpent > 30) { + await this.checkBlindBoxTrigger(); + } + } catch (error) { + wx.showToast({ + title: '上报失败', + icon: 'none' + }); } }, - loadNextCard() { - const currentCard = cloneCard(this.data.nextCard); - const nextCard = getNextMockCard(currentCard.id); + async loadNextCard() { + try { + const data = await request({ + url: `/api/v1/cards/recommend?limit=2&category=${encodeURIComponent(this.data.currentTab)}&sessionId=${ensureSessionId()}` + }); + const currentCard = data.items[0] ? normalizeCard(data.items[0]) : cloneCard(MOCK_CARDS[0]); + const nextCard = data.items[1] + ? normalizeCard(data.items[1]) + : getNextMockCard(currentCard.id); - this.setData({ - cardTranslateX: 0, - cardTranslateY: 0, - cardRotate: 0, - cardTransition: 'none', - previewScale: 0.978, - currentCard, - nextCard - }); + this.setData({ + cardTranslateX: 0, + cardTranslateY: 0, + cardRotate: 0, + cardTransition: 'none', + previewScale: 0.978, + currentCard, + nextCard + }); + } catch (error) { + const currentCard = cloneCard(this.data.nextCard); + const nextCard = getNextMockCard(currentCard.id); + + this.setData({ + cardTranslateX: 0, + cardTranslateY: 0, + cardRotate: 0, + cardTransition: 'none', + previewScale: 0.978, + currentCard, + nextCard + }); + } }, - checkBlindBoxTrigger() { - // Trigger Blind Box UI - wx.showToast({ - title: '触发盲盒弹窗!', - icon: 'none' - }); - // 重置计数,避免重复触发 - this.setData({ swipeSessionCount: 0 }); + async checkBlindBoxTrigger() { + try { + const result = await request({ + url: '/api/v1/matching/trigger-check', + method: 'POST', + data: { + sessionId: ensureSessionId(), + sessionSwipeCount: this.data.swipeSessionCount, + sessionDuration: Math.floor((Date.now() - this.data.entryTime) / 1000) + } + }); + + if (result.shouldTrigger && result.matchUser) { + wx.showToast({ + title: `匹配到 ${result.matchUser.name}`, + icon: 'none' + }); + } + + this.setData({ swipeSessionCount: 0 }); + } catch (error) { + wx.showToast({ + title: '盲盒检查失败', + icon: 'none' + }); + } }, openCommentPanel() { @@ -325,6 +402,7 @@ Page({ switchTab(e) { this.setData({ currentTab: e.currentTarget.dataset.tab }); + this.loadNextCard(); }, goToDiscovery() { diff --git a/pages/onboarding/onboarding.js b/pages/onboarding/onboarding.js index 25fbcfc..ab21b0d 100644 --- a/pages/onboarding/onboarding.js +++ b/pages/onboarding/onboarding.js @@ -1,12 +1,15 @@ +const { request } = require('../../utils/request'); + Page({ data: { currentIndex: 0, currentQuestion: {}, questions: [ - { id: 1, title: '你的生理性别是?', desc: '为了同频共振信号标定。', leftAns: '男性', rightAns: '女性' }, - { id: 2, title: '你的出生年龄段是?', desc: '代际不同,频段不同。', leftAns: '95后&00后', rightAns: '90后及更早' }, - { id: 3, title: '你当前的感情状态是?', desc: '了解你对建立羁绊的渴望期。', leftAns: '单身', rightAns: '非单身' } + { id: 1, title: '你的生理性别是?', desc: '为了同频共振信号标定。', leftAns: '男性', rightAns: '女性', leftValue: 'male', rightValue: 'female' }, + { id: 2, title: '你的出生年龄段是?', desc: '代际不同,频段不同。', leftAns: '95后&00后', rightAns: '90后及更早', leftValue: 'gen-z', rightValue: '90s' }, + { id: 3, title: '你当前的感情状态是?', desc: '了解你对建立羁绊的渴望期。', leftAns: '单身', rightAns: '非单身', leftValue: 'single', rightValue: 'not-single' } ], + answers: [], translateX: 0, rotate: 0, transition: 'none', @@ -39,17 +42,34 @@ Page({ forceSwipeLeft() { this.animateSwipe('left'); }, forceSwipeRight() { this.animateSwipe('right'); }, - animateSwipe(direction) { + async animateSwipe(direction) { const moveX = direction === 'right' ? 800 : -800; this.setData({ translateX: moveX, rotate: direction === 'right' ? 30 : -30, transition: 'transform 0.4s ease-out' }); - // 逻辑:保存记录 -> 延迟切换题目 - setTimeout(() => { this.nextQuestion(); }, 300); + + const currentQuestion = this.data.questions[this.data.currentIndex]; + const selected = + direction === 'right' ? currentQuestion.rightValue : currentQuestion.leftValue; + const answers = [...this.data.answers]; + const existingIndex = answers.findIndex((item) => item.questionId === currentQuestion.id); + const nextAnswer = { questionId: currentQuestion.id, selected }; + + if (existingIndex >= 0) { + answers[existingIndex] = nextAnswer; + } else { + answers.push(nextAnswer); + } + + this.setData({ answers }); + + setTimeout(async () => { + await this.nextQuestion(); + }, 300); }, - nextQuestion() { + async nextQuestion() { if (this.data.currentIndex < this.data.questions.length - 1) { const nextIdx = this.data.currentIndex + 1; this.setData({ @@ -58,11 +78,23 @@ Page({ translateX: 0, rotate: 0, transition: 'none' }); } else { - wx.redirectTo({ url: '/pages/home/home' }); + await this.submitAnswers(); } }, skipQuestion() { + const currentQuestion = this.data.questions[this.data.currentIndex]; + const answers = [...this.data.answers]; + const existingIndex = answers.findIndex((item) => item.questionId === currentQuestion.id); + const nextAnswer = { questionId: currentQuestion.id }; + + if (existingIndex >= 0) { + answers[existingIndex] = nextAnswer; + } else { + answers.push(nextAnswer); + } + + this.setData({ answers }); this.nextQuestion(); }, @@ -75,5 +107,23 @@ Page({ translateX: 0, rotate: 0, transition: 'none' }); } + }, + + async submitAnswers() { + try { + await request({ + url: '/api/v1/onboarding/submit', + method: 'POST', + data: { + answers: this.data.answers + } + }); + wx.redirectTo({ url: '/pages/home/home' }); + } catch (error) { + wx.showToast({ + title: '提交失败,请重试', + icon: 'none' + }); + } } }); diff --git a/pages/profile/profile.js b/pages/profile/profile.js index 1350a33..054d867 100644 --- a/pages/profile/profile.js +++ b/pages/profile/profile.js @@ -1,19 +1,39 @@ +const { request } = require('../../utils/request'); + Page({ data: { statusBarHeight: 20, windowHeight: 800, - userInfo: { - location: '13km Away', - mbti: 'INTJ', - matchScore: '89%' + profile: { + nickname: '', + gender: '', + age: '', + mbti: '', + signature: '', + photos: [], + counts: { + visitors: 0, + followers: 0, + following: 0, + interactions: 0 + } } }, - onLoad() { + async onLoad() { const windowInfo = wx.getWindowInfo(); this.setData({ statusBarHeight: windowInfo.statusBarHeight || 44, windowHeight: windowInfo.windowHeight }); + await this.loadProfile(); + }, + async loadProfile() { + try { + const profile = await request({ url: '/api/v1/profile/info' }); + this.setData({ profile }); + } catch (error) { + wx.showToast({ title: '档案加载失败', icon: 'none' }); + } }, goToHome() { wx.redirectTo({ url: '/pages/home/home' }); diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml index 68359a2..07b52c0 100644 --- a/pages/profile/profile.wxml +++ b/pages/profile/profile.wxml @@ -7,7 +7,7 @@ Same Wavelength - + @@ -17,27 +17,27 @@ - + - 林深处的麋鹿 + {{profile.nickname || '加载中'}} - ♀ 24 - INFJ-T + {{profile.gender || '-'}} {{profile.age || '-'}} + {{profile.mbti || '-'}} - "在喧嚣的世界寻找同频的声音,慢热却也深邃。" + "{{profile.signature || ''}}" - 1.2k访客 + {{profile.counts.visitors}}访客 - 458粉丝 + {{profile.counts.followers}}粉丝 - 89关注 + {{profile.counts.following}}关注 - 2.4k互动 + {{profile.counts.interactions}}互动 @@ -89,14 +89,10 @@ Photo Wall - 9 PHOTOS + {{profile.photos.length}} PHOTOS - - - - - + + diff --git a/utils/request.js b/utils/request.js new file mode 100644 index 0000000..d683f79 --- /dev/null +++ b/utils/request.js @@ -0,0 +1,30 @@ +const DEFAULT_BASE_URL = 'http://127.0.0.1:3005'; + +function getBaseUrl() { + const app = getApp(); + return app?.globalData?.apiBaseUrl || DEFAULT_BASE_URL; +} + +function request({ url, method = 'GET', data }) { + return new Promise((resolve, reject) => { + wx.request({ + url: `${getBaseUrl()}${url}`, + method, + data, + header: { + 'x-test-user-id': '1', + }, + success: (res) => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(res.data.data); + return; + } + + reject(new Error(`Request failed with status ${res.statusCode}`)); + }, + fail: reject, + }); + }); +} + +module.exports = { request }; diff --git a/utils/session.js b/utils/session.js new file mode 100644 index 0000000..f696dab --- /dev/null +++ b/utils/session.js @@ -0,0 +1,20 @@ +function ensureSessionId() { + const key = 'swSessionId'; + const existing = wx.getStorageSync(key); + if (existing) { + return existing; + } + + const next = `session-${Date.now()}`; + wx.setStorageSync(key, next); + return next; +} + +function resetSessionId() { + wx.removeStorageSync('swSessionId'); +} + +module.exports = { + ensureSessionId, + resetSessionId, +}; From 79b2a492f0e0478f24151629c9f34a22d84329f5 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 10 Apr 2026 15:20:03 +0900 Subject: [PATCH 4/6] chore: ignore local backend startup logs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e34f71c..bf5e910 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ .DS_Store project.private.config.json *.swp +backend/.main-start*.log backend/.env backend/.tmp-start*.log backend/dist/ From 42e5181007ae8809707351790ae7a988584d6b7d Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 14 Apr 2026 20:00:33 +0900 Subject: [PATCH 5/6] docs: add blind box mechanism design --- .../2026-04-14-blind-box-mechanism-design.md | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-14-blind-box-mechanism-design.md diff --git a/docs/superpowers/specs/2026-04-14-blind-box-mechanism-design.md b/docs/superpowers/specs/2026-04-14-blind-box-mechanism-design.md new file mode 100644 index 0000000..9e9e344 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-blind-box-mechanism-design.md @@ -0,0 +1,331 @@ +# 盲盒产品机制全量设计与分阶段落地方案 + +日期:2026-04-14 + +## 1. 背景 + +当前项目已经具备以下基础能力: + +- 真心话首页的卡片滑动与滑动结果落库 +- 基于 session 阈值的匹配触发检查 +- 连接关系创建 +- 小纸条会话创建与聊天页跳转 + +但当前“盲盒”仅停留在一个轻量触发壳子上: + +- 前端在首页达到阈值后会触发匹配检查 +- 后端会返回一个候选用户 +- 用户可直接建立连接并进入聊天 + +这与产品文档中的盲盒机制存在明显差距。文档希望盲盒承担的不只是“弹一个人”,而是完整承接以下链路: + +- 基于真心话站队行为触发匹配时机 +- 用更有仪式感的开盒方式展示候选人 +- 通过“想认识”机制控制私信权限,降低骚扰 +- 将用户从真心话平滑带入小纸条 + +本设计的目标是:先围绕“打开盲盒 -> 想认识 -> 进入小纸条”建立最小可用闭环,再逐步演进到文档中的完整版本。 + +## 2. 设计目标 + +### 2.1 业务目标 + +- 把真心话内的匹配触发,从“普通提示框”升级成明确的产品机制 +- 让用户形成“滑动站队会解锁社交机会”的认知 +- 建立安全、低压力、可控制的小纸条入口 + +### 2.2 当前阶段优先目标 + +本轮优先验证的核心价值为: + +- 用户能否顺畅完成“打开盲盒 -> 想认识 -> 匹配成功 -> 进入小纸条”的主闭环 + +### 2.3 非目标 + +本轮不要求一次做完以下能力: + +- 动态触发算法 +- 站队数据驱动的精细同频计算 +- 分享解锁、付费解锁 +- 超级心动盲盒、关怀盲盒 +- 完整的双向真实确认与访客/喜欢我体系 + +这些能力会被保留在后续版本设计中。 + +## 3. 当前实现与目标状态差距 + +### 3.1 当前实现 + +- 首页达到固定阈值后触发匹配检查 +- 匹配检查返回单个候选用户 +- 用户确认后直接创建连接 +- 当前连接状态更接近“单边 connect 即 connected” +- 成功后立刻创建会话并跳转小纸条 + +### 3.2 目标状态 + +- 盲盒为自动触发、无固定入口的独立机制 +- 打开盲盒后展示 3 个候选人 +- 用户通过“想认识”表达意愿 +- 只有双方都点“想认识”,才解锁小纸条 +- 盲盒后续支持普通/超级心动/关怀等类型化扩展 + +### 3.3 核心差距 + +- 缺少真正的盲盒容器与状态流转 +- 缺少 3 人候选展示层 +- 缺少“想认识”独立状态模型 +- 缺少双向匹配成功语义 +- 缺少盲盒分层版本规划 + +## 4. 全量产品设计 + +本节描述最终完整形态,供后续各版本逐步收敛。 + +### 4.1 触发机制 + +- 盲盒在真心话首页中自动触发,无固定入口 +- 用户在一段有效滑动行为后触发 +- 最终触发策略应从固定阈值升级为动态算法,综合考虑: + - 滑动数量 + - 有效站队数量 + - 停留时长 + - 用户历史开盒反馈 + - 近期社交反馈状况 + +### 4.2 盲盒类型 + +- 普通盲盒 + - 每日固定额度 + - 触发门槛与同频要求随当日触发次数递增 +- 超级心动盲盒 + - 仅在高同频情况下触发 + - 不占普通盲盒额度 +- 关怀盲盒 + - 面向弱反馈用户的保底触发机制 + +### 4.3 打开盲盒后的信息结构 + +- 展示 3 个候选人卡片 +- 卡片最小信息层可包含: + - 头像 + - 昵称 + - 同频标签 +- 后续增强信息可包括: + - 同频题数 + - 匹配理由 + - 城市标签 + - 轻量主页入口 + +### 4.4 想认识与解锁小纸条 + +- 用户点击候选人卡片上的“想认识”按钮,发出意愿 +- 只有双方都点“想认识”,才算匹配成功 +- 匹配成功后: + - 产生可进入小纸条的关系 + - 创建或解锁一对一会话 + - 进入小纸条 + +### 4.5 与小纸条的关系 + +- 小纸条中的会话列表,只应展示已成功匹配的关系 +- “喜欢我”“我喜欢的”“系统通知”等应与盲盒意愿状态打通 +- 通讯录应仅收录双方已成功匹配的对象 + +## 5. 分阶段落地方案 + +本方案采用“全量设计,分阶段交付”的方式推进。 + +--- + +## 6. MVP 版本 + +### 6.1 版本目标 + +验证最短主闭环: + +- 自动触发盲盒 +- 打开盲盒 +- 展示 3 个候选人 +- 点击“想认识” +- 看到“匹配成功”确认态 +- 进入小纸条 + +在 MVP 中,产品规则保持“双向都点想认识才进小纸条”,但技术实现先用“对方自动同意/模拟同意”兜底,保证闭环可跑通。 + +### 6.2 MVP P0 + +- 将首页当前匹配提示框升级为盲盒弹层 +- 保持“自动触发、无固定入口”的产品形态 +- 触发条件暂时使用固定阈值,而非动态算法 +- 用户点击“打开盲盒”后展示 3 个候选人卡片 +- 候选卡片仅展示: + - 头像 + - 昵称 + - 同频标签 + - 想认识按钮 +- 用户点击“想认识”后,系统以“模拟对方同意”的方式生成匹配成功结果 +- 页面进入“匹配成功”确认态 +- 用户从确认态点击进入小纸条 +- 系统为该关系创建会话或打开现有会话 +- 在小纸条侧能看到由盲盒转化而来的会话 +- 补齐最小埋点: + - 盲盒触发 + - 盲盒打开 + - 盲盒曝光 + - 点击想认识 + - 匹配成功 + - 进入小纸条 + +### 6.3 MVP P1 + +- 增加“稍后再说”后的本次 session 恢复能力 +- 增加基础去重,避免短时间重复开出相同用户 +- 增加候选失效、连接失败、会话创建失败的错误兜底 +- 增加基础频控,避免同一 session 过于频繁触发 +- 增加盲盒状态字段,区分: + - 已触发未打开 + - 已打开未操作 + - 已点想认识 + - 已匹配成功 + - 已进入小纸条 + +--- + +## 7. V1 版本 + +### 7.1 版本目标 + +将 MVP 中的“模拟成功闭环”升级为真实可追踪的双向社交流程,使盲盒成为小纸条安全关系的真实上游。 + +### 7.2 V1 P0 + +- 引入真实的“想认识”状态模型 +- 支持区分: + - 我发出的想认识 + - 我收到的想认识 + - 双方匹配成功 +- 将当前“单边 connect 直接 connected”的语义升级为双向确认语义 +- 仅当双方都点“想认识”时,才真正解锁小纸条 +- 增加系统通知: + - 有人想认识你 + - 你们已匹配成功 +- 保证一段成功关系只对应一个稳定会话,避免重复创建 +- 小纸条列表严格只展示双方已成功匹配的会话 + +### 7.3 V1 P1 + +- 增加“喜欢我 / 我喜欢的 / 匹配成功”分层列表 +- 在盲盒候选卡片上增加轻量主页或简版资料入口 +- 增加候选排序优化: + - 活跃度 + - 真人头像 + - 社交需求一致性 + - 同城优先 +- 增加隐藏、忽略、撤回等基础关系管理能力 +- 增加基础风控,限制异常频率的批量想认识操作 + +--- + +## 8. V2 版本 + +### 8.1 版本目标 + +让盲盒机制从“社交闭环入口”升级为产品文档中的完整匹配系统。 + +### 8.2 V2 P0 + +- 引入普通盲盒、超级心动盲盒、关怀盲盒三类机制 +- 引入脱单 / 交友 / 兴趣三种盲盒业务类型 +- 建立普通盲盒的递增门槛模型 +- 建立超级心动盲盒的高同频判定逻辑 +- 建立关怀盲盒的弱反馈保底逻辑 +- 将候选人生成逻辑升级为真正使用站队数据 +- 将触发策略从固定阈值升级为动态算法 +- 建立盲盒历史、触发记录、结果记录的完整数据链路 + +### 8.3 V2 P1 + +- 增加分享解锁剩余卡片等运营玩法 +- 增加盲盒相关付费能力 +- 增加匹配权重调节能力 +- 增加年龄、同城、排除标签等前置筛选规则 +- 增加盲盒实验位与策略配置能力 +- 增加完整数据面板: + - 触发率 + - 开盒率 + - 想认识率 + - 双向成功率 + - 小纸条转化率 + +## 9. 数据与状态建议 + +本节不是实现细节,而是后续建模方向。 + +### 9.1 建议拆分的对象 + +- 盲盒触发记录 + - 用于记录何时触发、触发类型、触发来源、触发时机 +- 盲盒候选曝光记录 + - 用于记录本次开盒展示了哪些候选人 +- 想认识意愿记录 + - 用于记录单边意愿 +- 匹配成功关系 + - 用于标记双向成功后的社交权限 + +### 9.2 为什么不能继续只靠当前 connection 模型 + +当前 connection 更适合描述“已经可以聊天”的结果,不适合承载完整盲盒过程中的中间状态。 +随着版本演进,需要把“曝光”“我想认识你”“你也想认识我”“已成功配对”拆开,否则 V1 和 V2 会很难扩展。 + +## 10. 风险与取舍 + +### 10.1 MVP 风险 + +- 如果首版就做真实双向确认,闭环很容易被状态流转拖慢 +- 如果首版卡片信息过多,会稀释“开盒 -> 想认识 -> 进小纸条”的核心验证目标 +- 如果首版就引入动态算法,会把问题从产品机制实现转移到算法调优 + +### 10.2 本方案的取舍 + +本方案有意选择: + +- 先闭环,再精细化 +- 先容器机制,再复杂算法 +- 先模拟双向成功,再升级真实双向 + +这是为了让团队先验证盲盒机制是否真的能成为小纸条的有效入口。 + +## 11. 推荐推进顺序 + +推荐按以下顺序执行: + +1. 先做 MVP P0,验证主闭环 +2. 再做 MVP P1,补齐稳定性和状态管理 +3. 再进入 V1 P0,替换自动同意为真实双向想认识 +4. 之后再逐步扩展 V1 P1 和 V2 + +## 12. 验收口径 + +### 12.1 MVP 验收 + +满足以下条件即可视为 MVP 主目标达成: + +- 用户在真心话中可被自动触发盲盒 +- 用户能打开盲盒并看到 3 个候选人 +- 用户可对候选人点击“想认识” +- 用户会看到“匹配成功”确认态 +- 用户可从确认态进入小纸条 +- 小纸条可看到并打开该会话 + +### 12.2 V1 验收 + +- 不再依赖自动同意兜底 +- 只有双方都点“想认识”时,才进入小纸条 +- 盲盒关系、通知、小纸条三者状态一致 + +### 12.3 V2 验收 + +- 盲盒类型化与递进式规则完整可用 +- 站队数据真正驱动候选生成与排序 +- 触发机制和转化指标可被持续优化 From a80fdcefbb5d46b0fe1e179294f9b58735d46563 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 14 Apr 2026 20:30:08 +0900 Subject: [PATCH 6/6] feat: add blind box mvp flow --- backend/package-lock.json | 1 + backend/prisma/seed.ts | 23 +-- backend/src/common/load-backend-env.ts | 38 +++++ backend/src/main.ts | 24 +-- backend/src/matching/matching.service.ts | 40 +++-- backend/test/env-loading.e2e-spec.ts | 44 +++++ backend/test/matching.e2e-spec.ts | 34 +++- backend/test/test-helpers.ts | 10 ++ docs/API.md | 22 ++- pages/home/home.js | 117 ++++++++++++- pages/home/home.wxml | 31 ++++ pages/home/home.wxss | 208 +++++++++++++++++++++++ 12 files changed, 521 insertions(+), 71 deletions(-) create mode 100644 backend/src/common/load-backend-env.ts create mode 100644 backend/test/env-loading.e2e-spec.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index ac3a83b..8905db9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -223,6 +223,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 1852a3e..661b9c2 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -1,27 +1,8 @@ -import fs from 'node:fs'; import path from 'node:path'; import { PrismaClient } from '@prisma/client'; +import { loadBackendEnv } from '../src/common/load-backend-env'; -const envPath = path.resolve(__dirname, '../.env'); -if (fs.existsSync(envPath)) { - const envText = fs.readFileSync(envPath, 'utf8'); - for (const line of envText.split(/\r?\n/)) { - if (!line || line.trim().startsWith('#')) { - continue; - } - - const separatorIndex = line.indexOf('='); - if (separatorIndex === -1) { - continue; - } - - const key = line.slice(0, separatorIndex).trim(); - const value = line.slice(separatorIndex + 1).trim(); - if (!(key in process.env)) { - process.env[key] = value; - } - } -} +loadBackendEnv(path.resolve(__dirname, '..')); const prisma = new PrismaClient(); diff --git a/backend/src/common/load-backend-env.ts b/backend/src/common/load-backend-env.ts new file mode 100644 index 0000000..9edf38d --- /dev/null +++ b/backend/src/common/load-backend-env.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +function applyEnvFile(filePath: string) { + const envText = fs.readFileSync(filePath, 'utf8'); + for (const line of envText.split(/\r?\n/)) { + if (!line || line.trim().startsWith('#')) { + continue; + } + + const separatorIndex = line.indexOf('='); + if (separatorIndex === -1) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + if (!(key in process.env)) { + process.env[key] = value; + } + } +} + +export function loadBackendEnv(baseDir = process.cwd()) { + const candidates = [ + path.resolve(baseDir, '.env'), + path.resolve(baseDir, '.env.example'), + ]; + + for (const filePath of candidates) { + if (fs.existsSync(filePath)) { + applyEnvFile(filePath); + return filePath; + } + } + + return null; +} diff --git a/backend/src/main.ts b/backend/src/main.ts index 9c2897b..a36ec40 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,31 +1,11 @@ import 'reflect-metadata'; -import fs from 'node:fs'; -import path from 'node:path'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpResponseInterceptor } from './common/http-response.interceptor'; +import { loadBackendEnv } from './common/load-backend-env'; -const envPath = path.resolve(process.cwd(), '.env'); -if (fs.existsSync(envPath)) { - const envText = fs.readFileSync(envPath, 'utf8'); - for (const line of envText.split(/\r?\n/)) { - if (!line || line.trim().startsWith('#')) { - continue; - } - - const separatorIndex = line.indexOf('='); - if (separatorIndex === -1) { - continue; - } - - const key = line.slice(0, separatorIndex).trim(); - const value = line.slice(separatorIndex + 1).trim(); - if (!(key in process.env)) { - process.env[key] = value; - } - } -} +loadBackendEnv(process.cwd()); export function configureApp(app: INestApplication) { app.setGlobalPrefix('api/v1'); diff --git a/backend/src/matching/matching.service.ts b/backend/src/matching/matching.service.ts index e43a77e..64848eb 100644 --- a/backend/src/matching/matching.service.ts +++ b/backend/src/matching/matching.service.ts @@ -59,51 +59,61 @@ export class MatchingService { }, }); - const best = candidates + const ranked = candidates .map((candidate) => { const candidateTopics = candidate.profileTags .filter((tag) => tag.tagType === 'topic_preference') .map((tag) => tag.tagValue); const sharedTopics = userTopics.filter((topic) => candidateTopics.includes(topic)); const score = sharedTopics.length * 25 + (candidate.avatarUrl ? 10 : 0) + 5; + const tags = sharedTopics.slice(0, 2); const reason = sharedTopics.length > 0 ? `你们都更关注${sharedTopics[0]}话题` : '你们最近的活跃与表达节奏接近'; - return { candidate, score, reason }; + return { candidate, score, reason, tags }; }) - .sort((a, b) => b.score - a.score)[0]; + .sort((a, b) => b.score - a.score); + const selected = ranked.slice(0, 3); + const primary = selected[0]; await this.prisma.userSession.update({ where: { sessionId: dto.sessionId }, - data: { blindBoxChecked: true, blindBoxTriggered: Boolean(best) }, + data: { blindBoxChecked: true, blindBoxTriggered: Boolean(primary) }, }); await this.prisma.matchEvent.create({ data: { userId, - candidateUserId: best?.candidate.id, + candidateUserId: primary?.candidate.id, sessionId: dto.sessionId, - triggerReason: best?.reason ?? 'no_candidate', - matchScore: best?.score ?? null, - resultStatus: best ? 'matched' : 'no_match', + triggerReason: primary?.reason ?? 'no_candidate', + matchScore: primary?.score ?? null, + resultStatus: primary ? 'matched' : 'no_match', }, }); - if (!best) { + if (!primary) { return { shouldTrigger: false, reason: 'no_candidate' }; } return { shouldTrigger: true, - matchUser: { - userId: best.candidate.id.toString(), - name: best.candidate.nickname, - avatar: best.candidate.avatarUrl ?? '', + blindBox: { + triggerMode: 'threshold', + title: '发现同频的人', + confirmText: '打开盲盒', + cancelText: '稍后再说', + candidates: selected.map((item) => ({ + userId: item.candidate.id.toString(), + nickname: item.candidate.nickname, + avatar: item.candidate.avatarUrl ?? '', + tags: item.tags.length > 0 ? item.tags : ['同频'], + })), }, - matchReason: best.reason, - matchScore: best.score, + matchReason: primary.reason, + matchScore: primary.score, }; } } diff --git a/backend/test/env-loading.e2e-spec.ts b/backend/test/env-loading.e2e-spec.ts new file mode 100644 index 0000000..ebcd2fe --- /dev/null +++ b/backend/test/env-loading.e2e-spec.ts @@ -0,0 +1,44 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { loadBackendEnv } from '../src/common/load-backend-env'; + +describe('backend env loading', () => { + it('falls back to .env.example when .env is missing', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sw-env-')); + const envExamplePath = path.join(tempDir, '.env.example'); + + fs.writeFileSync( + envExamplePath, + 'DATABASE_URL=postgresql://sw:sw@localhost:5434/sw?schema=public\nALLOW_TEST_AUTH=true\n', + 'utf8', + ); + + const oldDatabaseUrl = process.env.DATABASE_URL; + const oldAllowTestAuth = process.env.ALLOW_TEST_AUTH; + + delete process.env.DATABASE_URL; + delete process.env.ALLOW_TEST_AUTH; + + loadBackendEnv(tempDir); + + expect(process.env.DATABASE_URL).toBe( + 'postgresql://sw:sw@localhost:5434/sw?schema=public', + ); + expect(process.env.ALLOW_TEST_AUTH).toBe('true'); + + if (oldDatabaseUrl === undefined) { + delete process.env.DATABASE_URL; + } else { + process.env.DATABASE_URL = oldDatabaseUrl; + } + + if (oldAllowTestAuth === undefined) { + delete process.env.ALLOW_TEST_AUTH; + } else { + process.env.ALLOW_TEST_AUTH = oldAllowTestAuth; + } + + fs.rmSync(tempDir, { recursive: true, force: true }); + }); +}); diff --git a/backend/test/matching.e2e-spec.ts b/backend/test/matching.e2e-spec.ts index fcad166..4cdf457 100644 --- a/backend/test/matching.e2e-spec.ts +++ b/backend/test/matching.e2e-spec.ts @@ -23,7 +23,7 @@ describe('Matching (e2e)', () => { await app.close(); }); - it('returns a match candidate once session thresholds are met', async () => { + it('returns a blind box payload with three candidates once thresholds are met', async () => { await request(app.getHttpServer()) .post('/api/v1/cards/swipe') .set('x-test-user-id', '1') @@ -49,7 +49,18 @@ describe('Matching (e2e)', () => { .expect(201); expect(response.body.data).toMatchObject({ - shouldTrigger: expect.any(Boolean), + shouldTrigger: true, + blindBox: { + triggerMode: 'threshold', + candidates: expect.any(Array), + }, + }); + expect(response.body.data.blindBox.candidates).toHaveLength(3); + expect(response.body.data.blindBox.candidates[0]).toMatchObject({ + userId: expect.any(String), + nickname: expect.any(String), + avatar: expect.any(String), + tags: expect.any(Array), }); }); @@ -68,4 +79,23 @@ describe('Matching (e2e)', () => { expect(second.body.data).toMatchObject({ shouldTrigger: false }); }); + + it('keeps the no-trigger response when the same session was already checked', async () => { + await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-blind-box-2', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + const second = await request(app.getHttpServer()) + .post('/api/v1/matching/trigger-check') + .set('x-test-user-id', '1') + .send({ sessionId: 'session-blind-box-2', sessionSwipeCount: 3, sessionDuration: 45 }) + .expect(201); + + expect(second.body.data).toEqual({ + shouldTrigger: false, + reason: 'already_checked', + }); + }); }); diff --git a/backend/test/test-helpers.ts b/backend/test/test-helpers.ts index a569449..1805d2c 100644 --- a/backend/test/test-helpers.ts +++ b/backend/test/test-helpers.ts @@ -1,8 +1,18 @@ import { execSync } from 'node:child_process'; +import { loadBackendEnv } from '../src/common/load-backend-env'; export function resetDatabase() { + loadBackendEnv(process.cwd()); + + execSync('npx prisma migrate reset --force --skip-generate --skip-seed', { + cwd: process.cwd(), + stdio: 'ignore', + env: process.env, + }); + execSync('npm run prisma:seed', { cwd: process.cwd(), stdio: 'ignore', + env: process.env, }); } diff --git a/docs/API.md b/docs/API.md index b4df58e..d543f83 100644 --- a/docs/API.md +++ b/docs/API.md @@ -107,12 +107,13 @@ | 项 | 值 | |----|----| -| URL | `POST /api/v1/blind-box/trigger-check` | +| URL | `POST /api/v1/matching/trigger-check` | | 触发条件 | 本次会话累计 ≥ 3 次有效划动 且 在线时长 > 30 秒 | **Request Body** ```json { + "sessionId": "session-001", "sessionSwipeCount": 3, "sessionDuration": 45 } @@ -122,10 +123,19 @@ ```json { "shouldTrigger": true, - "matchUser": { - "userId": 302, - "name": "MARCUS", - "avatar": "https://..." + "blindBox": { + "triggerMode": "threshold", + "title": "发现同频的人", + "confirmText": "打开盲盒", + "cancelText": "稍后再说", + "candidates": [ + { + "userId": "302", + "nickname": "MARCUS", + "avatar": "https://...", + "tags": ["价值观"] + } + ] } } ``` @@ -234,7 +244,7 @@ | `POST /api/v1/onboarding/submit` | `onboarding.js animateSwipe` | 未实现,Mock | | `GET /api/v1/cards/recommend` | `home.js loadNextCard` | 未实现,Mock | | `POST /api/v1/cards/swipe` | `home.js recordSwipe` | 未实现,Mock | -| `POST /api/v1/blind-box/trigger-check` | `home.js checkBlindBoxTrigger` | 未实现,Mock | +| `POST /api/v1/matching/trigger-check` | `home.js checkBlindBoxTrigger` | 已实现,返回盲盒候选数据 | | `GET /api/v1/discovery/feed` | `discovery.js onLoad` | 未实现,Mock | | `POST /api/v1/discovery/publish` | `discovery.js openPublish` | 未实现,待开发 | | `GET /api/v1/profile/info` | `profile.js onLoad` | 未实现,Mock | diff --git a/pages/home/home.js b/pages/home/home.js index b37bef9..6db19d5 100644 --- a/pages/home/home.js +++ b/pages/home/home.js @@ -110,7 +110,15 @@ Page({ // Session tracking swipeSessionCount: 0, - entryTime: 0 + entryTime: 0, + showBlindBox: false, + blindBoxStage: 'closed', + blindBoxTitle: '发现同频的人', + blindBoxCandidates: [], + selectedBlindBoxCandidate: null, + blindBoxOpening: false, + blindBoxSubmitting: false, + blindBoxEnteringChat: false }, onLoad() { @@ -332,11 +340,21 @@ Page({ } }); - if (result.shouldTrigger && result.matchUser) { - wx.showToast({ - title: `匹配到 ${result.matchUser.name}`, - icon: 'none' + if (result.shouldTrigger && result.blindBox && Array.isArray(result.blindBox.candidates)) { + this.setData({ + showBlindBox: true, + blindBoxStage: 'prompt', + blindBoxTitle: result.blindBox.title || '发现同频的人', + blindBoxCandidates: result.blindBox.candidates.map((item) => ({ + ...item, + hasAvatar: Boolean(item.avatar) + })), + selectedBlindBoxCandidate: null, + blindBoxOpening: false, + blindBoxSubmitting: false, + blindBoxEnteringChat: false }); + return; } this.setData({ swipeSessionCount: 0 }); @@ -348,6 +366,93 @@ Page({ } }, + openBlindBox() { + if (this.data.blindBoxOpening) { + return; + } + + this.setData({ + blindBoxOpening: true, + blindBoxStage: 'candidates' + }); + }, + + closeBlindBox() { + this.setData({ + showBlindBox: false, + blindBoxStage: 'closed', + blindBoxTitle: '发现同频的人', + blindBoxCandidates: [], + selectedBlindBoxCandidate: null, + blindBoxOpening: false, + blindBoxSubmitting: false, + blindBoxEnteringChat: false, + swipeSessionCount: 0 + }); + }, + + async chooseBlindBoxCandidate(e) { + const userId = e.currentTarget.dataset.userId; + const candidate = this.data.blindBoxCandidates.find((item) => item.userId === userId); + if (!candidate || this.data.blindBoxSubmitting) { + return; + } + + this.setData({ blindBoxSubmitting: true }); + try { + const connection = await request({ + url: '/api/v1/matching/connections', + method: 'POST', + data: { + candidateUserId: candidate.userId, + action: 'connect' + } + }); + + this.setData({ + blindBoxStage: 'success', + blindBoxSubmitting: false, + selectedBlindBoxCandidate: { + ...candidate, + connectionId: connection.connectionId + } + }); + } catch (error) { + this.setData({ blindBoxSubmitting: false }); + wx.showToast({ + title: '想认识失败', + icon: 'none' + }); + } + }, + + async enterBlindBoxChat() { + const candidate = this.data.selectedBlindBoxCandidate; + if (!candidate || !candidate.connectionId || this.data.blindBoxEnteringChat) { + return; + } + + this.setData({ blindBoxEnteringChat: true }); + try { + const conversation = await request({ + url: '/api/v1/chat/conversations', + method: 'POST', + data: { connectionId: candidate.connectionId } + }); + + this.closeBlindBox(); + wx.redirectTo({ + url: `/pages/chat/chat?conversationId=${conversation.conversationId}` + }); + } catch (error) { + this.setData({ blindBoxEnteringChat: false }); + wx.showToast({ + title: '进入小纸条失败', + icon: 'none' + }); + } + }, + openCommentPanel() { wx.showToast({ title: '评论功能开发中', @@ -387,6 +492,8 @@ Page({ // Empty function to prevent touch propagation on modal }, + noop() {}, + toggleMenuPanel() { this.setData({ showMenuPanel: !this.data.showMenuPanel }); }, diff --git a/pages/home/home.wxml b/pages/home/home.wxml index b09f105..86a4cfa 100644 --- a/pages/home/home.wxml +++ b/pages/home/home.wxml @@ -211,4 +211,35 @@ + + + + {{blindBoxTitle}} + 你解锁了 3 位同频候选人,打开看看谁最想认识。 + + + + + + 选择想认识的人 + + + + + {{item.nickname}} + {{item.tags[0]}} + + + + + + + 匹配成功 + 你和 {{selectedBlindBoxCandidate.nickname}} 已互相想认识。 + + + + + + diff --git a/pages/home/home.wxss b/pages/home/home.wxss index 0386661..c7106ed 100644 --- a/pages/home/home.wxss +++ b/pages/home/home.wxss @@ -579,3 +579,211 @@ page { font-weight: bold; font-size: 30rpx; } + +.blind-box-mask { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(18, 18, 18, 0.56); + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + box-sizing: border-box; +} + +.blind-box-panel { + width: 100%; + max-width: 640rpx; + background: #FFFDF7; + border-radius: 36rpx; + padding: 40rpx 32rpx; + box-sizing: border-box; + box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.18); +} + +.blind-box-stage { + display: flex; + flex-direction: column; +} + +.blind-box-title { + font-size: 36rpx; + font-weight: bold; + color: #1A1A1A; + text-align: center; +} + +.blind-box-desc { + margin-top: 20rpx; + font-size: 26rpx; + line-height: 1.6; + color: rgba(26, 26, 26, 0.68); + text-align: center; +} + +.blind-box-candidate-list { + display: flex; + flex-direction: column; + gap: 20rpx; + margin-top: 28rpx; +} + +.blind-box-candidate { + display: flex; + flex-direction: column; + align-items: center; + padding: 28rpx 24rpx; + border-radius: 24rpx; + background: #FFFFFF; + border: 1rpx solid rgba(26, 26, 26, 0.06); +} + +.blind-box-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 32rpx; +} + +.blind-box-avatar-placeholder { + background: linear-gradient(180deg, #ECE7DD 0%, #DFD8C8 100%); +} + +.blind-box-name { + margin-top: 18rpx; + font-size: 28rpx; + font-weight: bold; + color: #1A1A1A; +} + +.blind-box-tag { + margin-top: 10rpx; + font-size: 22rpx; + color: rgba(26, 26, 26, 0.56); +} + +.blind-box-primary { + margin-top: 28rpx; + background: #1A1A1A; + color: #FFF; + border-radius: 999rpx; + font-size: 28rpx; + font-weight: bold; +} + +.blind-box-primary.small { + width: 100%; +} + +.blind-box-secondary { + margin-top: 18rpx; + background: #FFFFFF; + color: #1A1A1A; + border: 1rpx solid rgba(26, 26, 26, 0.12); + border-radius: 999rpx; + font-size: 26rpx; +} + +.blind-box-mask { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(18, 18, 18, 0.56); + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + box-sizing: border-box; +} + +.blind-box-panel { + width: 100%; + max-width: 640rpx; + background: #FFFDF7; + border-radius: 36rpx; + padding: 40rpx 32rpx; + box-sizing: border-box; + box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.18); +} + +.blind-box-stage { + display: flex; + flex-direction: column; +} + +.blind-box-title { + font-size: 36rpx; + font-weight: bold; + color: #1A1A1A; + text-align: center; +} + +.blind-box-desc { + margin-top: 20rpx; + font-size: 26rpx; + line-height: 1.6; + color: rgba(26, 26, 26, 0.68); + text-align: center; +} + +.blind-box-candidate-list { + display: flex; + flex-direction: column; + gap: 20rpx; + margin-top: 28rpx; +} + +.blind-box-candidate { + display: flex; + flex-direction: column; + align-items: center; + padding: 28rpx 24rpx; + border-radius: 24rpx; + background: #FFFFFF; + border: 1rpx solid rgba(26, 26, 26, 0.06); +} + +.blind-box-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 32rpx; +} + +.blind-box-avatar-placeholder { + background: linear-gradient(180deg, #ECE7DD 0%, #DFD8C8 100%); +} + +.blind-box-name { + margin-top: 18rpx; + font-size: 28rpx; + font-weight: bold; + color: #1A1A1A; +} + +.blind-box-tag { + margin-top: 10rpx; + font-size: 22rpx; + color: rgba(26, 26, 26, 0.56); +} + +.blind-box-primary { + margin-top: 28rpx; + background: #1A1A1A; + color: #FFF; + border-radius: 999rpx; + font-size: 28rpx; + font-weight: bold; +} + +.blind-box-primary.small { + width: 100%; +} + +.blind-box-secondary { + margin-top: 18rpx; + background: #FFFFFF; + color: #1A1A1A; + border: 1rpx solid rgba(26, 26, 26, 0.12); + border-radius: 999rpx; + font-size: 26rpx; +}