From 6d918315c7a48a724c1bcde62a2171752946635f Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 7 Dec 2025 16:09:39 +0100 Subject: [PATCH] feat(auth): add fraud detection, cron jobs, and admin endpoints to referral system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add FraudDetectionService with IP/device fingerprinting, velocity checks, email pattern detection, and review queue management - Add ReferralCronService for retention checks (hourly), daily stats aggregation, rate limit cleanup, and weekly tier recalculation - Add ReferralsAdminController with endpoints for review queue, fraud patterns, and user referral management - Integrate referral initialization into user registration flow (auto-create referral code, initialize tier, apply referral code) - Add @nestjs/schedule dependency for cron jobs - Export referrals schema from db/schema/index.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pnpm-lock.yaml | 831 +++++++----------- services/mana-core-auth/package.json | 1 + services/mana-core-auth/src/app.module.ts | 2 + .../mana-core-auth/src/auth/auth.module.ts | 4 +- .../src/auth/services/better-auth.service.ts | 71 +- .../src/auth/types/better-auth.types.ts | 2 + .../mana-core-auth/src/db/schema/index.ts | 1 + .../referrals/referrals-admin.controller.ts | 193 ++++ .../src/referrals/referrals.module.ts | 24 +- .../services/fraud-detection.service.ts | 642 ++++++++++++++ .../src/referrals/services/index.ts | 2 + .../services/referral-cron.service.ts | 327 +++++++ .../services/referral-tier.service.ts | 23 +- 13 files changed, 1615 insertions(+), 508 deletions(-) create mode 100644 services/mana-core-auth/src/referrals/referrals-admin.controller.ts create mode 100644 services/mana-core-auth/src/referrals/services/fraud-detection.service.ts create mode 100644 services/mana-core-auth/src/referrals/services/referral-cron.service.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f24ad81b..66a999cfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,7 +116,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -149,7 +149,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -173,14 +173,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -189,13 +189,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) + version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) + version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -528,19 +528,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -1221,6 +1221,9 @@ importers: '@babel/core': specifier: ^7.20.0 version: 7.28.5 + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 '@types/react': specifier: ~18.3.12 version: 18.3.27 @@ -4156,7 +4159,7 @@ importers: version: 1.57.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.1) vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -4210,6 +4213,9 @@ importers: '@nestjs/platform-express': specifier: ^10.4.15 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/schedule': + specifier: ^4.1.2 + version: 4.1.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) '@nestjs/throttler': specifier: ^6.2.1 version: 6.4.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2) @@ -4273,7 +4279,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.12(@types/node@22.19.1) + version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -4327,10 +4333,10 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6545,7 +6551,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -19707,16 +19713,6 @@ snapshots: transitivePeerDependencies: - ts-node - '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': - dependencies: - astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) - autoprefixer: 10.4.22(postcss@8.5.6) - postcss: 8.5.6 - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) - tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - ts-node - '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -22230,7 +22226,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) + expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22307,7 +22303,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e) + expo-router: 6.0.15(ohit2up6tuxb3x34brxduivol4) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -23542,6 +23538,41 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 + '@jest/core@29.7.0': + 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': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.1) + 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.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 @@ -23577,78 +23608,6 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': - 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': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - 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.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 30.2.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-resolve-dependencies: 30.2.0 - jest-runner: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - jest-watcher: 30.2.0 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 @@ -24073,32 +24032,6 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.19.12)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - 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(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) - 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(esbuild@0.19.12) - webpack-node-externals: 3.0.0 - transitivePeerDependencies: - - esbuild - - uglify-js - - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -24125,7 +24058,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@11.0.12(@types/node@22.19.1)': + '@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -24136,14 +24069,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) glob: 12.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2 + webpack: 5.100.2(esbuild@0.19.12) webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -26698,7 +26631,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -26708,7 +26641,20 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + optional: true + + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': @@ -27222,16 +27168,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -27280,15 +27226,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -27380,14 +27326,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -27419,14 +27365,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -27552,12 +27498,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -27588,12 +27534,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -27775,15 +27721,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -27814,13 +27760,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript @@ -28621,108 +28567,6 @@ snapshots: transitivePeerDependencies: - supports-color - astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): - dependencies: - '@astrojs/compiler': 2.13.0 - '@astrojs/internal-helpers': 0.7.5 - '@astrojs/markdown-remark': 6.3.9 - '@astrojs/telemetry': 3.3.0 - '@capsizecss/unpack': 3.0.1 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - boxen: 8.0.1 - ci-info: 4.3.1 - clsx: 2.1.1 - common-ancestor-path: 1.0.1 - cookie: 1.1.0 - cssesc: 3.0.0 - debug: 4.4.3 - deterministic-object-hash: 2.0.2 - devalue: 5.5.0 - diff: 5.2.0 - dlv: 1.1.3 - dset: 3.1.4 - es-module-lexer: 1.7.0 - esbuild: 0.25.12 - estree-walker: 3.0.3 - flattie: 1.1.1 - fontace: 0.3.1 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - import-meta-resolve: 4.2.0 - js-yaml: 4.1.1 - magic-string: 0.30.21 - magicast: 0.5.1 - mrmime: 2.0.1 - neotraverse: 0.6.18 - p-limit: 6.2.0 - p-queue: 8.1.1 - package-manager-detector: 1.5.0 - piccolore: 0.1.3 - picomatch: 4.0.3 - prompts: 2.4.2 - rehype: 13.0.2 - semver: 7.7.3 - shiki: 3.15.0 - smol-toml: 1.5.2 - svgo: 4.0.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tsconfck: 3.1.6(typescript@5.9.3) - ultrahtml: 1.6.0 - unifont: 0.6.0 - unist-util-visit: 5.0.0 - unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.8.2) - vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) - xxhash-wasm: 1.1.0 - yargs-parser: 21.1.1 - yocto-spinner: 0.2.3 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) - zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) - optionalDependencies: - sharp: 0.34.5 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - idb-keyval - - ioredis - - jiti - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - yaml - astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -29944,13 +29788,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.10.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -31008,11 +30852,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - semver: 7.7.3 - eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31060,14 +30899,14 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31092,17 +30931,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -31165,12 +31004,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -31207,20 +31046,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.48.0 - astro-eslint-parser: 1.2.2 - eslint: 9.39.1(jiti@1.21.7) - eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@1.21.7)) - globals: 16.5.0 - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 - transitivePeerDependencies: - - supports-color - eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@2.6.1)): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -31241,6 +31066,12 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-utils: 2.1.0 + regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31294,7 +31125,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -31303,9 +31134,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31317,7 +31148,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -31420,6 +31251,16 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) + eslint-utils: 2.1.0 + ignore: 5.3.2 + minimatch: 3.1.2 + resolve: 1.22.11 + semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31450,6 +31291,16 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31474,6 +31325,10 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31504,6 +31359,28 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.39.1(jiti@1.21.7) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -32623,7 +32500,54 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): + expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + optional: true + + expo-router@6.0.15(ohit2up6tuxb3x34brxduivol4): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -32657,12 +32581,12 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)) transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@types/react' @@ -33503,23 +33427,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -33554,6 +33461,23 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.100.2(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -33571,23 +33495,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.9.3 - webpack: 5.100.2 - form-data-encoder@1.7.2: {} form-data@3.0.4: @@ -34754,16 +34661,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@24.10.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -34773,15 +34680,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -34851,6 +34758,36 @@ snapshots: - ts-node optional: true + jest-config@29.7.0(@types/node@22.19.1): + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -34882,38 +34819,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - 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.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.10.1): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -34939,12 +34845,11 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.10.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -34971,9 +34876,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.19.1 - esbuild-register: 3.6.0(esbuild@0.19.12) - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + '@types/node': 20.19.25 + esbuild-register: 3.6.0(esbuild@0.27.0) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -35622,24 +35526,24 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -39406,6 +39310,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack: 5.100.2(esbuild@0.19.12) + webpack-sources: 3.3.3 + optional: true + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: acorn-loose: 8.5.2 @@ -39415,16 +39329,6 @@ snapshots: webpack: 5.100.2(esbuild@0.27.0) webpack-sources: 3.3.3 - react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - acorn-loose: 8.5.2 - neo-async: 2.6.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - webpack: 5.97.1(esbuild@0.19.12) - webpack-sources: 3.3.3 - optional: true - react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.1.0): dependencies: get-nonce: 1.0.1 @@ -40676,14 +40580,14 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.97.1(esbuild@0.19.12) + webpack: 5.100.2(esbuild@0.19.12) optionalDependencies: esbuild: 0.19.12 @@ -40709,15 +40613,6 @@ snapshots: optionalDependencies: esbuild: 0.27.0 - terser-webpack-plugin@5.3.14(webpack@5.100.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.100.2 - terser-webpack-plugin@5.3.14(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -40879,6 +40774,27 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.19.12 + jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -40900,25 +40816,15 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 semver: 7.7.3 - type-fest: 4.41.0 + source-map: 0.7.6 typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - jest-util: 30.2.0 + webpack: 5.100.2(esbuild@0.19.12) ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: @@ -40930,26 +40836,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.100.2 - - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) - ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -41529,23 +41415,6 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 - vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.53.3 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.25 - fsevents: 2.3.3 - jiti: 1.21.7 - lightningcss: 1.30.2 - terser: 5.44.1 - tsx: 4.20.6 - yaml: 2.8.1 - vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -41648,10 +41517,6 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): - optionalDependencies: - vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -41908,7 +41773,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.100.2: + webpack@5.100.2(esbuild@0.19.12): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -41932,7 +41797,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.100.2) + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -42002,36 +41867,6 @@ snapshots: - esbuild - uglify-js - webpack@5.97.1(esbuild@0.19.12): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.28.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/mana-core-auth/package.json b/services/mana-core-auth/package.json index 0e6b69c5c..ae517f1d9 100644 --- a/services/mana-core-auth/package.json +++ b/services/mana-core-auth/package.json @@ -24,6 +24,7 @@ "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", + "@nestjs/schedule": "^4.1.2", "@nestjs/throttler": "^6.2.1", "bcrypt": "^5.1.1", "better-auth": "^1.4.3", diff --git a/services/mana-core-auth/src/app.module.ts b/services/mana-core-auth/src/app.module.ts index af720f490..41404bbd4 100644 --- a/services/mana-core-auth/src/app.module.ts +++ b/services/mana-core-auth/src/app.module.ts @@ -6,6 +6,7 @@ import configuration from './config/configuration'; import { AuthModule } from './auth/auth.module'; import { CreditsModule } from './credits/credits.module'; import { FeedbackModule } from './feedback/feedback.module'; +import { ReferralsModule } from './referrals/referrals.module'; import { SettingsModule } from './settings/settings.module'; import { AiModule } from './ai/ai.module'; import { HealthModule } from './health/health.module'; @@ -28,6 +29,7 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; CreditsModule, FeedbackModule, HealthModule, + ReferralsModule, SettingsModule, ], providers: [ diff --git a/services/mana-core-auth/src/auth/auth.module.ts b/services/mana-core-auth/src/auth/auth.module.ts index 3b4e2aa16..fc5242d95 100644 --- a/services/mana-core-auth/src/auth/auth.module.ts +++ b/services/mana-core-auth/src/auth/auth.module.ts @@ -1,8 +1,10 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { BetterAuthService } from './services/better-auth.service'; +import { ReferralsModule } from '../referrals/referrals.module'; @Module({ + imports: [forwardRef(() => ReferralsModule)], controllers: [AuthController], providers: [BetterAuthService], exports: [BetterAuthService], diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index 30474196a..11cfce791 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -19,12 +19,18 @@ import { NotFoundException, ForbiddenException, UnauthorizedException, + Inject, + forwardRef, + Optional, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { createBetterAuth } from '../better-auth.config'; import type { BetterAuthInstance } from '../better-auth.config'; import { getDb } from '../../db/connection'; import { balances, organizationBalances } from '../../db/schema/credits.schema'; +import { ReferralCodeService } from '../../referrals/services/referral-code.service'; +import { ReferralTierService } from '../../referrals/services/referral-tier.service'; +import { ReferralTrackingService } from '../../referrals/services/referral-tracking.service'; import { hasUser, hasToken, hasMember, hasMembers, hasSession } from '../types/better-auth.types'; import type { RegisterB2CDto, @@ -91,7 +97,18 @@ export class BetterAuthService { return this.auth.api as unknown as BetterAuthAPI; } - constructor(private configService: ConfigService) { + constructor( + private configService: ConfigService, + @Optional() + @Inject(forwardRef(() => ReferralCodeService)) + private referralCodeService: ReferralCodeService, + @Optional() + @Inject(forwardRef(() => ReferralTierService)) + private referralTierService: ReferralTierService, + @Optional() + @Inject(forwardRef(() => ReferralTrackingService)) + private referralTrackingService: ReferralTrackingService + ) { this.databaseUrl = this.configService.get('database.url')!; this.auth = createBetterAuth(this.databaseUrl); } @@ -127,6 +144,9 @@ export class BetterAuthService { // Create personal credit balance await this.createPersonalCreditBalance(user.id); + // Initialize referral system for new user + await this.initializeUserReferrals(user.id, dto.referralCode, dto.sourceAppId); + return { user: { id: user.id, @@ -947,4 +967,53 @@ export class BetterAuthService { .replace(/--+/g, '-') // Replace multiple hyphens with single .trim(); } + + /** + * Initialize referral system for a new user + * + * This method: + * 1. Creates an automatic referral code for the new user + * 2. Initializes the user's tier (bronze) + * 3. If a referral code was used, applies the referral relationship + * + * @param userId - The new user's ID + * @param referralCode - Optional referral code used during signup + * @param sourceAppId - Optional app ID where the user registered + * @private + */ + private async initializeUserReferrals( + userId: string, + referralCode?: string, + sourceAppId?: string + ): Promise { + // Skip if referral services are not available + if (!this.referralCodeService || !this.referralTierService) { + return; + } + + try { + // 1. Create automatic referral code for the new user + await this.referralCodeService.createAutoCode(userId); + + // 2. Initialize user's tier (starts at bronze) + await this.referralTierService.initializeUserTier(userId); + + // 3. If a referral code was provided, apply the referral relationship + if (referralCode && this.referralTrackingService) { + // The applyReferral method handles validation internally + const result = await this.referralTrackingService.applyReferral({ + refereeId: userId, + code: referralCode, + sourceAppId: sourceAppId || 'manacore', + }); + + if (!result.success) { + console.warn('[initializeUserReferrals] Failed to apply referral code:', result.error); + } + } + } catch (error) { + // Log but don't fail registration if referral setup fails + console.error('[initializeUserReferrals] Error setting up referrals:', error); + } + } } diff --git a/services/mana-core-auth/src/auth/types/better-auth.types.ts b/services/mana-core-auth/src/auth/types/better-auth.types.ts index 0a20415b9..2fca555b4 100644 --- a/services/mana-core-auth/src/auth/types/better-auth.types.ts +++ b/services/mana-core-auth/src/auth/types/better-auth.types.ts @@ -374,6 +374,8 @@ export interface RegisterB2CDto { email: string; password: string; name: string; + referralCode?: string; + sourceAppId?: string; } /** diff --git a/services/mana-core-auth/src/db/schema/index.ts b/services/mana-core-auth/src/db/schema/index.ts index d7f72ca80..55f92cb1d 100644 --- a/services/mana-core-auth/src/db/schema/index.ts +++ b/services/mana-core-auth/src/db/schema/index.ts @@ -2,3 +2,4 @@ export * from './auth.schema'; export * from './credits.schema'; export * from './feedback.schema'; export * from './organizations.schema'; +export * from './referrals.schema'; diff --git a/services/mana-core-auth/src/referrals/referrals-admin.controller.ts b/services/mana-core-auth/src/referrals/referrals-admin.controller.ts new file mode 100644 index 000000000..0872a5f06 --- /dev/null +++ b/services/mana-core-auth/src/referrals/referrals-admin.controller.ts @@ -0,0 +1,193 @@ +/** + * Referrals Admin Controller + * + * Admin-only endpoints for managing the referral system: + * - Review queue management + * - Fraud pattern management + * - Statistics and reporting + */ + +import { Controller, Get, Post, Body, Param, Query, HttpCode, HttpStatus } from '@nestjs/common'; +import { FraudDetectionService } from './services/fraud-detection.service'; +import { ReferralTrackingService } from './services/referral-tracking.service'; + +// DTOs for admin endpoints +class ProcessReviewDto { + decision: 'approved' | 'rejected'; + reviewerId: string; + notes?: string; +} + +class AddFraudPatternDto { + patternType: 'email_domain' | 'ip_range' | 'device_pattern'; + patternValue: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + scoreImpact: number; + description: string; + createdBy: string; +} + +class PaginationQuery { + limit?: string; + offset?: string; +} + +// Note: In production, add proper auth guard +// @UseGuards(AdminAuthGuard) +@Controller('referrals/admin') +export class ReferralsAdminController { + constructor( + private fraudDetectionService: FraudDetectionService, + private trackingService: ReferralTrackingService + ) {} + + // =================================== + // REVIEW QUEUE ENDPOINTS + // =================================== + + /** + * Get pending review items + * GET /referrals/admin/reviews + */ + @Get('reviews') + async getPendingReviews(@Query() query: PaginationQuery) { + const limit = parseInt(query.limit || '50', 10); + const offset = parseInt(query.offset || '0', 10); + + const reviews = await this.fraudDetectionService.getPendingReviews(limit, offset); + + return { + items: reviews, + pagination: { + limit, + offset, + }, + }; + } + + /** + * Process a review decision + * POST /referrals/admin/reviews/:id/process + */ + @Post('reviews/:id/process') + @HttpCode(HttpStatus.OK) + async processReview(@Param('id') reviewId: string, @Body() dto: ProcessReviewDto) { + await this.fraudDetectionService.processReview( + reviewId, + dto.decision, + dto.reviewerId, + dto.notes + ); + + return { + success: true, + message: `Review ${dto.decision}`, + }; + } + + // =================================== + // FRAUD PATTERN ENDPOINTS + // =================================== + + /** + * Add a new fraud pattern + * POST /referrals/admin/fraud-patterns + */ + @Post('fraud-patterns') + @HttpCode(HttpStatus.CREATED) + async addFraudPattern(@Body() dto: AddFraudPatternDto) { + await this.fraudDetectionService.addFraudPattern( + dto.patternType, + dto.patternValue, + dto.severity, + dto.scoreImpact, + dto.description, + dto.createdBy + ); + + return { + success: true, + message: 'Fraud pattern added', + }; + } + + // =================================== + // STATISTICS ENDPOINTS + // =================================== + + /** + * Get fraud statistics + * GET /referrals/admin/stats/fraud + */ + @Get('stats/fraud') + async getFraudStats() { + return this.fraudDetectionService.getFraudStats(); + } + + /** + * Get overall referral statistics + * GET /referrals/admin/stats/overview + */ + @Get('stats/overview') + async getOverviewStats() { + return { + message: 'Overview stats endpoint - to be implemented with aggregated data', + }; + } + + // =================================== + // USER MANAGEMENT ENDPOINTS + // =================================== + + /** + * Get referral details for a specific user + * GET /referrals/admin/users/:userId/referrals + */ + @Get('users/:userId/referrals') + async getUserReferrals( + @Param('userId') userId: string, + @Query('status') status: string | undefined, + @Query() query: PaginationQuery + ) { + const limit = parseInt(query.limit || '50', 10); + const offset = parseInt(query.offset || '0', 10); + + const result = await this.trackingService.getReferredUsers(userId, status, limit, offset); + + return result; + } + + /** + * Get referral stats for a specific user + * GET /referrals/admin/users/:userId/stats + */ + @Get('users/:userId/stats') + async getUserStats(@Param('userId') userId: string) { + return this.trackingService.getReferralStats(userId); + } + + // =================================== + // MANUAL ACTIONS + // =================================== + + /** + * Manually trigger stage update (for support/admin use) + * POST /referrals/admin/manual/stage-update + */ + @Post('manual/stage-update') + @HttpCode(HttpStatus.OK) + async manualStageUpdate( + @Body() dto: { userId: string; stage: 'activated' | 'qualified'; appId?: string } + ) { + if (dto.stage === 'activated') { + await this.trackingService.checkActivation(dto.userId); + } else if (dto.stage === 'qualified') { + await this.trackingService.checkQualification(dto.userId); + } + + return { + success: true, + message: `Stage update processed for user ${dto.userId}`, + }; + } +} diff --git a/services/mana-core-auth/src/referrals/referrals.module.ts b/services/mana-core-auth/src/referrals/referrals.module.ts index 3c9721094..baba892ca 100644 --- a/services/mana-core-auth/src/referrals/referrals.module.ts +++ b/services/mana-core-auth/src/referrals/referrals.module.ts @@ -6,19 +6,35 @@ * - Referral code management * - Referral tracking and stage progression * - Tier calculation and bonus multipliers + * - Fraud detection and prevention */ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; import { ReferralsController } from './referrals.controller'; +import { ReferralsAdminController } from './referrals-admin.controller'; import { ReferralCodeService } from './services/referral-code.service'; import { ReferralTierService } from './services/referral-tier.service'; import { ReferralTrackingService } from './services/referral-tracking.service'; +import { FraudDetectionService } from './services/fraud-detection.service'; +import { ReferralCronService } from './services/referral-cron.service'; @Module({ - imports: [ConfigModule], - controllers: [ReferralsController], - providers: [ReferralCodeService, ReferralTierService, ReferralTrackingService], - exports: [ReferralCodeService, ReferralTierService, ReferralTrackingService], + imports: [ConfigModule, ScheduleModule.forRoot()], + controllers: [ReferralsController, ReferralsAdminController], + providers: [ + ReferralCodeService, + ReferralTierService, + ReferralTrackingService, + FraudDetectionService, + ReferralCronService, + ], + exports: [ + ReferralCodeService, + ReferralTierService, + ReferralTrackingService, + FraudDetectionService, + ], }) export class ReferralsModule {} diff --git a/services/mana-core-auth/src/referrals/services/fraud-detection.service.ts b/services/mana-core-auth/src/referrals/services/fraud-detection.service.ts new file mode 100644 index 000000000..5403ea602 --- /dev/null +++ b/services/mana-core-auth/src/referrals/services/fraud-detection.service.ts @@ -0,0 +1,642 @@ +/** + * Fraud Detection Service + * + * Handles fraud detection for the referral system: + * - Device fingerprinting and tracking + * - IP address analysis + * - Pattern detection (velocity, clusters) + * - Fraud scoring + * - Auto-hold and review queue management + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { eq, and, sql, gte, count, desc, or } from 'drizzle-orm'; +import { getDb } from '../../db/connection'; +import { + fingerprints, + userFingerprints, + fraudPatterns, + rateLimits, + reviewQueue, + relationships, + FRAUD_THRESHOLDS, + FRAUD_SIGNALS, + RATE_LIMITS, + type ReviewQueueItem, +} from '../../db/schema/referrals.schema'; +import * as crypto from 'crypto'; + +/** + * Fraud check input data + */ +export interface FraudCheckInput { + userId: string; + referrerId?: string; + ipAddress?: string; + userAgent?: string; + deviceFingerprint?: string; + email?: string; +} + +/** + * Fraud check result + */ +export interface FraudCheckResult { + score: number; + signals: string[]; + action: 'allow' | 'hold' | 'reject'; + holdReason?: string; +} + +/** + * Fingerprint data for storage + */ +export interface FingerprintData { + ipAddress: string; + deviceHash?: string; + userAgent?: string; +} + +@Injectable() +export class FraudDetectionService { + private readonly logger = new Logger(FraudDetectionService.name); + + constructor(private configService: ConfigService) {} + + private getDb() { + const databaseUrl = this.configService.get('database.url'); + return getDb(databaseUrl!); + } + + /** + * Hash a value for privacy (GDPR compliance) + */ + private hashValue(value: string): string { + return crypto.createHash('sha256').update(value).digest('hex'); + } + + /** + * Perform comprehensive fraud check for a referral + */ + async checkFraud(input: FraudCheckInput): Promise { + const signals: string[] = []; + let score = 0; + + try { + // 1. Check IP-based signals + if (input.ipAddress) { + const ipSignals = await this.checkIpSignals(input.ipAddress, input.referrerId); + signals.push(...ipSignals.signals); + score += ipSignals.score; + } + + // 2. Check device fingerprint signals + if (input.deviceFingerprint) { + const fpSignals = await this.checkFingerprintSignals( + input.deviceFingerprint, + input.referrerId + ); + signals.push(...fpSignals.signals); + score += fpSignals.score; + } + + // 3. Check referrer velocity (too many referrals too fast) + if (input.referrerId) { + const velocitySignals = await this.checkReferrerVelocity(input.referrerId); + signals.push(...velocitySignals.signals); + score += velocitySignals.score; + } + + // 4. Check email patterns + if (input.email) { + const emailSignals = this.checkEmailPatterns(input.email); + signals.push(...emailSignals.signals); + score += emailSignals.score; + } + + // 5. Check for known fraud patterns + const patternSignals = await this.checkKnownPatterns(input); + signals.push(...patternSignals.signals); + score += patternSignals.score; + + // Determine action based on score + let action: 'allow' | 'hold' | 'reject' = 'allow'; + let holdReason: string | undefined; + + if (score >= FRAUD_THRESHOLDS.critical) { + action = 'reject'; + } else if (score >= FRAUD_THRESHOLDS.highRisk) { + action = 'hold'; + holdReason = signals.join(', '); + } + + this.logger.debug( + `Fraud check for user ${input.userId}: score=${score}, action=${action}, signals=${signals.join(', ')}` + ); + + return { score, signals, action, holdReason }; + } catch (error) { + this.logger.error('Error during fraud check:', error); + // On error, allow but flag for review + return { + score: FRAUD_THRESHOLDS.highRisk, + signals: ['check_error'], + action: 'hold', + holdReason: 'Fraud check encountered an error', + }; + } + } + + /** + * Check IP-based fraud signals + */ + private async checkIpSignals( + ipAddress: string, + referrerId?: string + ): Promise<{ score: number; signals: string[] }> { + const db = this.getDb(); + const signals: string[] = []; + let score = 0; + const ipHash = this.hashValue(ipAddress); + + // Check how many users registered from this IP + const [ipCount] = await db + .select({ count: count() }) + .from(fingerprints) + .where(eq(fingerprints.ipHash, ipHash)); + + if (ipCount.count >= 5) { + signals.push('same_ip'); + score += FRAUD_SIGNALS.same_ip; + } + + // Check if IP was used by referrer + if (referrerId) { + const [referrerIP] = await db + .select() + .from(userFingerprints) + .innerJoin(fingerprints, eq(userFingerprints.fingerprintId, fingerprints.id)) + .where(and(eq(userFingerprints.userId, referrerId), eq(fingerprints.ipHash, ipHash))) + .limit(1); + + if (referrerIP) { + signals.push('same_ip'); + score += FRAUD_SIGNALS.same_ip; + } + } + + // Check if IP is from known proxy/VPN ranges + if (this.isProxyIP(ipAddress)) { + signals.push('vpn_proxy'); + score += FRAUD_SIGNALS.vpn_proxy; + } + + return { score, signals }; + } + + /** + * Check device fingerprint signals + */ + private async checkFingerprintSignals( + deviceHash: string, + referrerId?: string + ): Promise<{ score: number; signals: string[] }> { + const db = this.getDb(); + const signals: string[] = []; + let score = 0; + + // Check how many users share this device + const [fpCount] = await db + .select({ count: count() }) + .from(userFingerprints) + .innerJoin(fingerprints, eq(userFingerprints.fingerprintId, fingerprints.id)) + .where(eq(fingerprints.deviceHash, deviceHash)); + + if (fpCount.count >= 3) { + signals.push('same_device'); + score += FRAUD_SIGNALS.same_device; + } + + // Check if device was used by referrer + if (referrerId) { + const [referrerDevice] = await db + .select() + .from(userFingerprints) + .innerJoin(fingerprints, eq(userFingerprints.fingerprintId, fingerprints.id)) + .where( + and(eq(userFingerprints.userId, referrerId), eq(fingerprints.deviceHash, deviceHash)) + ) + .limit(1); + + if (referrerDevice) { + signals.push('same_device'); + score += FRAUD_SIGNALS.same_device; + } + } + + return { score, signals }; + } + + /** + * Check referrer velocity (too many referrals too fast) + */ + private async checkReferrerVelocity( + referrerId: string + ): Promise<{ score: number; signals: string[] }> { + const db = this.getDb(); + const signals: string[] = []; + let score = 0; + + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + // Check referrals in last day + const [dailyCount] = await db + .select({ count: count() }) + .from(relationships) + .where( + and(eq(relationships.referrerId, referrerId), gte(relationships.createdAt, oneDayAgo)) + ); + + if (dailyCount.count >= RATE_LIMITS.registrationsPerReferrer.limit) { + signals.push('rapid_registration'); + score += FRAUD_SIGNALS.rapid_registration; + } + + if (dailyCount.count >= 10) { + signals.push('bulk_registrations'); + score += FRAUD_SIGNALS.bulk_registrations; + } + + return { score, signals }; + } + + /** + * Check email patterns for fraud indicators + */ + private checkEmailPatterns(email: string): { score: number; signals: string[] } { + const signals: string[] = []; + let score = 0; + + const lowerEmail = email.toLowerCase(); + + // Check for disposable email domains + const disposableDomains = [ + 'tempmail.com', + 'throwaway.com', + 'guerrillamail.com', + '10minutemail.com', + 'mailinator.com', + 'yopmail.com', + 'fakeinbox.com', + 'trashmail.com', + ]; + + const domain = lowerEmail.split('@')[1]; + if (disposableDomains.some((d) => domain?.includes(d))) { + signals.push('disposable_email'); + score += FRAUD_SIGNALS.disposable_email; + } + + // Check for plus-addressing pattern abuse (test+1@gmail.com) + if (lowerEmail.includes('+') && /\+\d+@/.test(lowerEmail)) { + signals.push('similar_email'); + score += FRAUD_SIGNALS.similar_email; + } + + return { score, signals }; + } + + /** + * Check for known fraud patterns in database + */ + private async checkKnownPatterns( + input: FraudCheckInput + ): Promise<{ score: number; signals: string[] }> { + const db = this.getDb(); + const signals: string[] = []; + let score = 0; + + if (!input.email) { + return { score, signals }; + } + + const domain = input.email.split('@')[1]; + if (!domain) { + return { score, signals }; + } + + // Check for known bad email domains + const patterns = await db + .select() + .from(fraudPatterns) + .where( + and( + eq(fraudPatterns.isActive, true), + eq(fraudPatterns.patternType, 'email_domain'), + eq(fraudPatterns.patternValue, domain) + ) + ); + + for (const pattern of patterns) { + signals.push(`known_pattern_${pattern.patternType}`); + score += pattern.scoreImpact; + } + + return { score, signals }; + } + + /** + * Simple check for proxy/VPN IPs + */ + private isProxyIP(_ip: string): boolean { + // In production, use services like IPQualityScore, MaxMind, or IP2Proxy + // For now, return false (disabled) + return false; + } + + /** + * Store device fingerprint for a user + */ + async storeFingerprint(userId: string, data: FingerprintData): Promise { + const db = this.getDb(); + + try { + const ipHash = this.hashValue(data.ipAddress); + const deviceHash = data.deviceHash || null; + const userAgentHash = data.userAgent ? this.hashValue(data.userAgent) : null; + + // Check if fingerprint already exists + let [existingFp] = await db + .select() + .from(fingerprints) + .where( + and( + eq(fingerprints.ipHash, ipHash), + deviceHash + ? eq(fingerprints.deviceHash, deviceHash) + : sql`${fingerprints.deviceHash} IS NULL` + ) + ) + .limit(1); + + if (!existingFp) { + // Create new fingerprint + [existingFp] = await db + .insert(fingerprints) + .values({ + ipHash, + deviceHash, + userAgentHash, + firstSeenAt: new Date(), + lastSeenAt: new Date(), + registrationCount: 1, + }) + .returning(); + } else { + // Update existing + await db + .update(fingerprints) + .set({ + lastSeenAt: new Date(), + registrationCount: sql`${fingerprints.registrationCount} + 1`, + }) + .where(eq(fingerprints.id, existingFp.id)); + } + + // Link fingerprint to user (check if exists first) + const [existingLink] = await db + .select() + .from(userFingerprints) + .where( + and( + eq(userFingerprints.userId, userId), + eq(userFingerprints.fingerprintId, existingFp.id) + ) + ) + .limit(1); + + if (!existingLink) { + await db.insert(userFingerprints).values({ + userId, + fingerprintId: existingFp.id, + seenAt: new Date(), + context: 'registration', + }); + } + } catch (error) { + this.logger.error('Error storing fingerprint:', error); + } + } + + /** + * Add item to review queue + */ + async addToReviewQueue( + relationshipId: string, + fraudScore: number, + signals: string[], + _reason: string + ): Promise { + const db = this.getDb(); + + try { + const priority = + fraudScore >= FRAUD_THRESHOLDS.critical + ? 'critical' + : fraudScore >= FRAUD_THRESHOLDS.highRisk + ? 'high' + : fraudScore >= FRAUD_THRESHOLDS.mediumRisk + ? 'medium' + : 'low'; + + await db.insert(reviewQueue).values({ + relationshipId, + fraudScore, + fraudSignals: JSON.stringify(signals), + priority, + status: 'pending', + createdAt: new Date(), + }); + } catch (error) { + this.logger.error('Error adding to review queue:', error); + } + } + + /** + * Get pending review items + */ + async getPendingReviews(limit: number = 50, offset: number = 0): Promise { + const db = this.getDb(); + + return db + .select() + .from(reviewQueue) + .where(eq(reviewQueue.status, 'pending')) + .orderBy(desc(reviewQueue.fraudScore), reviewQueue.createdAt) + .limit(limit) + .offset(offset); + } + + /** + * Process review decision + */ + async processReview( + reviewId: string, + decision: 'approved' | 'rejected', + _reviewerId: string, + notes?: string + ): Promise { + const db = this.getDb(); + + await db + .update(reviewQueue) + .set({ + status: decision, + reviewedAt: new Date(), + notes, + }) + .where(eq(reviewQueue.id, reviewId)); + + // Get review to find relationship + const [review] = await db + .select() + .from(reviewQueue) + .where(eq(reviewQueue.id, reviewId)) + .limit(1); + + if (!review) return; + + if (decision === 'approved') { + // Reset fraud score + await db + .update(relationships) + .set({ fraudScore: 0, isFlagged: false }) + .where(eq(relationships.id, review.relationshipId)); + } else if (decision === 'rejected') { + // Mark as fraudulent + await db + .update(relationships) + .set({ fraudScore: 100, isFlagged: true }) + .where(eq(relationships.id, review.relationshipId)); + } + } + + /** + * Add a fraud pattern to the database + */ + async addFraudPattern( + patternType: 'email_domain' | 'ip_range' | 'device_pattern', + patternValue: string, + severity: 'low' | 'medium' | 'high' | 'critical', + scoreImpact: number, + description: string, + createdBy: string + ): Promise { + const db = this.getDb(); + + await db.insert(fraudPatterns).values({ + patternType, + patternValue, + severity, + scoreImpact, + description, + createdBy, + isActive: true, + createdAt: new Date(), + }); + } + + /** + * Check rate limit for an action + */ + async checkRateLimit( + identifier: string, + identifierType: string, + action: string, + limit: number, + windowMinutes: number + ): Promise<{ allowed: boolean; remaining: number }> { + const db = this.getDb(); + + const windowStart = new Date(Date.now() - windowMinutes * 60 * 1000); + const windowEnd = new Date(Date.now() + windowMinutes * 60 * 1000); + + const [record] = await db + .select() + .from(rateLimits) + .where( + and( + eq(rateLimits.identifier, identifier), + eq(rateLimits.identifierType, identifierType), + eq(rateLimits.action, action), + gte(rateLimits.windowStart, windowStart) + ) + ) + .limit(1); + + if (!record) { + // Create new rate limit record + await db.insert(rateLimits).values({ + identifier, + identifierType, + action, + count: 1, + windowStart: new Date(), + windowEnd, + }); + + return { allowed: true, remaining: limit - 1 }; + } + + if (record.count >= limit) { + return { allowed: false, remaining: 0 }; + } + + // Increment count + await db + .update(rateLimits) + .set({ count: record.count + 1 }) + .where(eq(rateLimits.id, record.id)); + + return { allowed: true, remaining: limit - record.count - 1 }; + } + + /** + * Get fraud statistics for admin dashboard + */ + async getFraudStats(): Promise<{ + pendingReviews: number; + rejectedToday: number; + flaggedReferrals: number; + }> { + const db = this.getDb(); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // Count pending reviews + const [pendingCount] = await db + .select({ count: count() }) + .from(reviewQueue) + .where(eq(reviewQueue.status, 'pending')); + + // Count rejected today + const [rejectedCount] = await db + .select({ count: count() }) + .from(reviewQueue) + .where(and(eq(reviewQueue.status, 'rejected'), gte(reviewQueue.reviewedAt, today))); + + // Count flagged referrals + const [flaggedCount] = await db + .select({ count: count() }) + .from(relationships) + .where(eq(relationships.isFlagged, true)); + + return { + pendingReviews: pendingCount.count, + rejectedToday: rejectedCount.count, + flaggedReferrals: flaggedCount.count, + }; + } +} diff --git a/services/mana-core-auth/src/referrals/services/index.ts b/services/mana-core-auth/src/referrals/services/index.ts index 37a297c9c..178f86f75 100644 --- a/services/mana-core-auth/src/referrals/services/index.ts +++ b/services/mana-core-auth/src/referrals/services/index.ts @@ -1,3 +1,5 @@ export { ReferralCodeService } from './referral-code.service'; export { ReferralTierService } from './referral-tier.service'; export { ReferralTrackingService } from './referral-tracking.service'; +export { FraudDetectionService } from './fraud-detection.service'; +export { ReferralCronService } from './referral-cron.service'; diff --git a/services/mana-core-auth/src/referrals/services/referral-cron.service.ts b/services/mana-core-auth/src/referrals/services/referral-cron.service.ts new file mode 100644 index 000000000..7a6e7362e --- /dev/null +++ b/services/mana-core-auth/src/referrals/services/referral-cron.service.ts @@ -0,0 +1,327 @@ +/** + * Referral Cron Service + * + * Handles scheduled tasks for the referral system: + * - Retention checking (30-day mark) + * - Daily statistics aggregation + * - Cleanup of expired rate limits + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { ConfigService } from '@nestjs/config'; +import { eq, and, sql, lte, gte, count, isNull } from 'drizzle-orm'; +import { getDb } from '../../db/connection'; +import { + relationships, + bonusEvents, + dailyStats, + rateLimits, + userTiers, + BONUS_AMOUNTS, + TIMING_RULES, +} from '../../db/schema/referrals.schema'; +import { balances } from '../../db/schema/credits.schema'; +import { ReferralTierService } from './referral-tier.service'; + +@Injectable() +export class ReferralCronService { + private readonly logger = new Logger(ReferralCronService.name); + + constructor( + private configService: ConfigService, + private tierService: ReferralTierService + ) {} + + private getDb() { + const databaseUrl = this.configService.get('database.url'); + return getDb(databaseUrl!); + } + + /** + * Check for retained referrals (30 days after qualification) + * Runs every hour + */ + @Cron(CronExpression.EVERY_HOUR) + async processRetentionCheck(): Promise { + this.logger.log('Starting retention check...'); + const db = this.getDb(); + + try { + const retentionDays = TIMING_RULES.retentionCheckDays; + const retentionDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); + + // Find qualified referrals that are now retained + const eligibleReferrals = await db + .select() + .from(relationships) + .where( + and( + eq(relationships.status, 'qualified'), + lte(relationships.qualifiedAt, retentionDate), + isNull(relationships.retainedAt) + ) + ) + .limit(100); + + let processedCount = 0; + let errorCount = 0; + + for (const referral of eligibleReferrals) { + try { + await this.processRetention(referral); + processedCount++; + } catch (error) { + errorCount++; + this.logger.error(`Error processing retention for referral ${referral.id}:`, error); + } + } + + this.logger.log( + `Retention check complete: ${processedCount} processed, ${errorCount} errors` + ); + } catch (error) { + this.logger.error('Error in retention check:', error); + } + } + + /** + * Process a single retention transition + */ + private async processRetention(referral: typeof relationships.$inferSelect): Promise { + const db = this.getDb(); + + // Get referrer's tier for multiplier + const multiplier = await this.tierService.getMultiplier(referral.referrerId); + const baseBonus = BONUS_AMOUNTS.retained.referrer; + const finalBonus = Math.round(baseBonus * multiplier); + + // Get referrer's current tier + const tierInfo = await this.tierService.getUserTier(referral.referrerId); + + // Update relationship to retained + await db + .update(relationships) + .set({ + status: 'retained', + retainedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(relationships.id, referral.id)); + + // Award retention bonus to referrer (if not held for fraud) + if (referral.fraudScore < 50) { + await db + .update(balances) + .set({ + balance: sql`${balances.balance} + ${finalBonus}`, + totalEarned: sql`${balances.totalEarned} + ${finalBonus}`, + }) + .where(eq(balances.userId, referral.referrerId)); + + // Record bonus event + await db.insert(bonusEvents).values({ + relationshipId: referral.id, + userId: referral.referrerId, + eventType: 'retained', + creditsBase: baseBonus, + tierMultiplier: multiplier, + creditsFinal: finalBonus, + tierAtTime: tierInfo.current, + status: 'paid', + createdAt: new Date(), + }); + } else { + // Record as held due to fraud score + await db.insert(bonusEvents).values({ + relationshipId: referral.id, + userId: referral.referrerId, + eventType: 'retained', + creditsBase: baseBonus, + tierMultiplier: multiplier, + creditsFinal: finalBonus, + tierAtTime: tierInfo.current, + status: 'held', + holdReason: `High fraud score: ${referral.fraudScore}`, + createdAt: new Date(), + }); + } + } + + /** + * Aggregate daily statistics + * Runs at midnight every day + */ + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async aggregateDailyStats(): Promise { + this.logger.log('Starting daily stats aggregation...'); + const db = this.getDb(); + + try { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // Count registrations + const [registrationsResult] = await db + .select({ count: count() }) + .from(relationships) + .where(and(gte(relationships.createdAt, yesterday), lte(relationships.createdAt, today))); + + // Count activations + const [activationsResult] = await db + .select({ count: count() }) + .from(relationships) + .where( + and(gte(relationships.activatedAt, yesterday), lte(relationships.activatedAt, today)) + ); + + // Count qualifications + const [qualificationsResult] = await db + .select({ count: count() }) + .from(relationships) + .where( + and(gte(relationships.qualifiedAt, yesterday), lte(relationships.qualifiedAt, today)) + ); + + // Count retentions + const [retentionsResult] = await db + .select({ count: count() }) + .from(relationships) + .where(and(gte(relationships.retainedAt, yesterday), lte(relationships.retainedAt, today))); + + // Sum credits paid + const [creditsPaidResult] = await db + .select({ total: sql`COALESCE(SUM(${bonusEvents.creditsFinal}), 0)` }) + .from(bonusEvents) + .where( + and( + eq(bonusEvents.status, 'paid'), + gte(bonusEvents.createdAt, yesterday), + lte(bonusEvents.createdAt, today) + ) + ); + + // Sum credits held + const [creditsHeldResult] = await db + .select({ total: sql`COALESCE(SUM(${bonusEvents.creditsFinal}), 0)` }) + .from(bonusEvents) + .where( + and( + eq(bonusEvents.status, 'held'), + gte(bonusEvents.createdAt, yesterday), + lte(bonusEvents.createdAt, today) + ) + ); + + // Count fraud blocked + const [fraudBlockedResult] = await db + .select({ count: count() }) + .from(relationships) + .where( + and( + gte(relationships.fraudScore, 90), + gte(relationships.createdAt, yesterday), + lte(relationships.createdAt, today) + ) + ); + + // Insert daily stats + await db.insert(dailyStats).values({ + date: yesterday, + registrations: registrationsResult.count, + activations: activationsResult.count, + qualifications: qualificationsResult.count, + retentions: retentionsResult.count, + creditsPaid: creditsPaidResult.total || 0, + creditsHeld: creditsHeldResult.total || 0, + fraudBlocked: fraudBlockedResult.count, + }); + + this.logger.log('Daily stats aggregation complete'); + } catch (error) { + this.logger.error('Error aggregating daily stats:', error); + } + } + + /** + * Cleanup expired rate limits + * Runs every 6 hours + */ + @Cron(CronExpression.EVERY_6_HOURS) + async cleanupRateLimits(): Promise { + this.logger.log('Cleaning up expired rate limits...'); + const db = this.getDb(); + + try { + await db.delete(rateLimits).where(lte(rateLimits.windowEnd, new Date())); + + this.logger.log('Rate limit cleanup complete'); + } catch (error) { + this.logger.error('Error cleaning up rate limits:', error); + } + } + + /** + * Recalculate tier standings for all users + * Runs weekly on Sunday at 3am + */ + @Cron('0 3 * * 0') + async recalculateTiers(): Promise { + this.logger.log('Recalculating all user tiers...'); + const db = this.getDb(); + + try { + // Get all user tiers + const allTiers = await db.select().from(userTiers); + + let updatedCount = 0; + + for (const userTier of allTiers) { + // Recalculate qualified count from relationships + const [actualCount] = await db + .select({ count: count() }) + .from(relationships) + .where( + and( + eq(relationships.referrerId, userTier.userId), + eq(relationships.status, 'qualified') + ) + ); + + // Add retained counts too + const [retainedCount] = await db + .select({ count: count() }) + .from(relationships) + .where( + and(eq(relationships.referrerId, userTier.userId), eq(relationships.status, 'retained')) + ); + + const totalQualified = actualCount.count + retainedCount.count; + + // Update if different + if (totalQualified !== userTier.qualifiedCount) { + const newTier = this.tierService.calculateTierFromCount(totalQualified); + + await db + .update(userTiers) + .set({ + qualifiedCount: totalQualified, + tier: newTier, + updatedAt: new Date(), + }) + .where(eq(userTiers.userId, userTier.userId)); + + updatedCount++; + } + } + + this.logger.log(`Tier recalculation complete: ${updatedCount} users updated`); + } catch (error) { + this.logger.error('Error recalculating tiers:', error); + } + } +} diff --git a/services/mana-core-auth/src/referrals/services/referral-tier.service.ts b/services/mana-core-auth/src/referrals/services/referral-tier.service.ts index c6aa04eca..624fd501d 100644 --- a/services/mana-core-auth/src/referrals/services/referral-tier.service.ts +++ b/services/mana-core-auth/src/referrals/services/referral-tier.service.ts @@ -80,12 +80,27 @@ export class ReferralTierService { } /** - * Get the multiplier for a given tier + * Get the multiplier for a given tier name */ - getMultiplier(tier: TierName): number { + getMultiplierForTier(tier: TierName): number { return TIER_CONFIG[tier]?.multiplier || 1.0; } + /** + * Get the multiplier for a user by their ID + */ + async getMultiplier(userId: string): Promise { + const db = this.getDb(); + + const [tier] = await db.select().from(userTiers).where(eq(userTiers.userId, userId)).limit(1); + + if (!tier) { + return 1.0; // Default bronze multiplier + } + + return this.getMultiplierForTier(tier.tier as TierName); + } + /** * Calculate bonus credits with tier multiplier */ @@ -96,7 +111,7 @@ export class ReferralTierService { ): { base: number; multiplier: number; final: number } { const bonusConfig = BONUS_AMOUNTS[eventType]; const base = isReferrer ? bonusConfig.referrer : bonusConfig.referee; - const multiplier = isReferrer ? this.getMultiplier(tier) : 1.0; // Referee doesn't get tier bonus + const multiplier = isReferrer ? this.getMultiplierForTier(tier) : 1.0; // Referee doesn't get tier bonus const final = Math.round(base * multiplier); return { base, multiplier, final }; @@ -203,7 +218,7 @@ export class ReferralTierService { /** * Calculate tier from qualified count */ - private calculateTierFromCount(count: number): TierName { + calculateTierFromCount(count: number): TierName { if (count >= TIER_CONFIG.platinum.minQualified) return 'platinum'; if (count >= TIER_CONFIG.gold.minQualified) return 'gold'; if (count >= TIER_CONFIG.silver.minQualified) return 'silver';