mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
🐛 fix(auth): use Better Auth native JWT validation with EdDSA
- Replace jsonwebtoken RS256 validation with jose EdDSA - Add JWKS endpoint to expose Better Auth public keys - Use createRemoteJWKSet for token validation - Fix issuer mismatch (use env var consistently) - Add jwks table to schema for Better Auth JWT plugin - Install jose library for JWT verification
This commit is contained in:
parent
2a002bf6be
commit
8dd1e4326c
10 changed files with 573 additions and 555 deletions
473
pnpm-lock.yaml
generated
473
pnpm-lock.yaml
generated
|
|
@ -686,7 +686,7 @@ importers:
|
|||
version: 17.0.7(expo@54.0.25)(react@19.1.0)
|
||||
expo-router:
|
||||
specifier: ~6.0.14
|
||||
version: 6.0.15(hsxvh2bq7p37jxljkgxire2dbu)
|
||||
version: 6.0.15(r6e3zsmutyjzazpntzawr4wuhy)
|
||||
expo-secure-store:
|
||||
specifier: ~15.0.7
|
||||
version: 15.0.7(expo@54.0.25)
|
||||
|
|
@ -789,7 +789,7 @@ importers:
|
|||
version: 7.28.5
|
||||
'@testing-library/react-native':
|
||||
specifier: ^13.3.3
|
||||
version: 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)
|
||||
version: 13.3.3(jest@29.7.0)(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)
|
||||
'@types/jest':
|
||||
specifier: ^29.5.12
|
||||
version: 29.5.14
|
||||
|
|
@ -807,10 +807,10 @@ importers:
|
|||
version: 10.0.0
|
||||
jest:
|
||||
specifier: ^29.2.1
|
||||
version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
version: 29.7.0
|
||||
jest-expo:
|
||||
specifier: ~54.0.13
|
||||
version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(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)(webpack@5.100.2)
|
||||
version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0)(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)(webpack@5.100.2)
|
||||
patch-package:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.1
|
||||
|
|
@ -3277,7 +3277,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)
|
||||
|
|
@ -3310,7 +3310,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.97.1(esbuild@0.27.0))
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3)
|
||||
|
|
@ -4103,7 +4103,7 @@ importers:
|
|||
version: 0.5.21
|
||||
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)
|
||||
|
|
@ -4764,7 +4764,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)
|
||||
|
|
@ -4845,6 +4845,9 @@ importers:
|
|||
helmet:
|
||||
specifier: ^8.0.0
|
||||
version: 8.1.0
|
||||
jose:
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
|
|
@ -4875,7 +4878,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)
|
||||
|
|
@ -4929,10 +4932,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)
|
||||
|
|
@ -25575,7 +25578,7 @@ snapshots:
|
|||
wrap-ansi: 7.0.0
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
expo-router: 6.0.15(hsxvh2bq7p37jxljkgxire2dbu)
|
||||
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'
|
||||
|
|
@ -27085,6 +27088,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)(ts-node@10.9.2(@types/node@22.19.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@29.7.0(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
|
|
@ -27155,41 +27193,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.27.0))':
|
||||
dependencies:
|
||||
'@jest/console': 30.2.0
|
||||
|
|
@ -27666,32 +27669,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)
|
||||
|
|
@ -27718,7 +27695,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)
|
||||
|
|
@ -27729,14 +27706,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'
|
||||
|
|
@ -32017,6 +31994,19 @@ snapshots:
|
|||
redent: 3.0.0
|
||||
optionalDependencies:
|
||||
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@29.7.0)(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
|
||||
pretty-format: 30.2.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-test-renderer: 19.1.0(react@19.1.0)
|
||||
redent: 3.0.0
|
||||
optionalDependencies:
|
||||
jest: 29.7.0
|
||||
|
||||
'@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)':
|
||||
dependencies:
|
||||
|
|
@ -35369,6 +35359,21 @@ snapshots:
|
|||
crc-32: 1.2.2
|
||||
readable-stream: 4.7.0
|
||||
|
||||
create-jest@29.7.0:
|
||||
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@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3))
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
|
|
@ -35399,13 +35404,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:
|
||||
|
|
@ -38472,7 +38477,7 @@ snapshots:
|
|||
- '@types/react-dom'
|
||||
- supports-color
|
||||
|
||||
expo-router@6.0.15(hsxvh2bq7p37jxljkgxire2dbu):
|
||||
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
|
||||
|
|
@ -38511,12 +38516,13 @@ snapshots:
|
|||
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.100.2)
|
||||
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'
|
||||
- '@types/react-dom'
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
expo-router@6.0.15(qjp3usx4acoq47dkosl6pmu254):
|
||||
dependencies:
|
||||
|
|
@ -38564,6 +38570,52 @@ snapshots:
|
|||
- '@types/react-dom'
|
||||
- supports-color
|
||||
|
||||
expo-router@6.0.15(r6e3zsmutyjzazpntzawr4wuhy):
|
||||
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
|
||||
'@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.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)
|
||||
'@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-navigation/native-stack': 7.8.0(@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-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)
|
||||
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.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)
|
||||
expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@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.5(@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.5(@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.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)
|
||||
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.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@29.7.0)(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.100.2)
|
||||
transitivePeerDependencies:
|
||||
- '@react-native-masked-view/masked-view'
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
- supports-color
|
||||
|
||||
expo-router@6.0.15(ucgv42olhsnvykdrhhfuls4dzq):
|
||||
dependencies:
|
||||
'@expo/metro-runtime': 6.1.2(expo@54.0.12)(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)
|
||||
|
|
@ -39654,23 +39706,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
|
||||
|
|
@ -39705,6 +39740,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
|
||||
|
|
@ -39722,23 +39774,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@2.5.5:
|
||||
|
|
@ -41106,6 +41141,25 @@ snapshots:
|
|||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-cli@29.7.0:
|
||||
dependencies:
|
||||
'@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
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3))
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3))
|
||||
|
|
@ -41144,16 +41198,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
|
||||
|
|
@ -41314,38 +41368,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
|
||||
|
|
@ -41371,7 +41394,6 @@ 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
|
||||
|
|
@ -41515,7 +41537,7 @@ snapshots:
|
|||
jest-util: 30.2.0
|
||||
jest-validate: 30.2.0
|
||||
|
||||
jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(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)(webpack@5.100.2):
|
||||
jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.7.0)(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)(webpack@5.100.2):
|
||||
dependencies:
|
||||
'@expo/config': 12.0.10
|
||||
'@expo/json-file': 10.0.7
|
||||
|
|
@ -41526,7 +41548,7 @@ snapshots:
|
|||
jest-environment-jsdom: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-watch-select-projects: 2.0.0
|
||||
jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
|
||||
jest-watch-typeahead: 2.2.1(jest@29.7.0)
|
||||
json5: 2.2.3
|
||||
lodash: 4.17.21
|
||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
|
|
@ -41917,11 +41939,11 @@ snapshots:
|
|||
chalk: 3.0.0
|
||||
prompts: 2.4.2
|
||||
|
||||
jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))):
|
||||
jest-watch-typeahead@2.2.1(jest@29.7.0):
|
||||
dependencies:
|
||||
ansi-escapes: 6.2.1
|
||||
chalk: 4.1.2
|
||||
jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
jest-watcher: 29.7.0
|
||||
slash: 5.1.0
|
||||
|
|
@ -41994,6 +42016,18 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0:
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
|
|
@ -42006,12 +42040,12 @@ 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
|
||||
|
|
@ -46947,6 +46981,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
|
||||
|
|
@ -48505,14 +48549,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
|
||||
|
||||
|
|
@ -48757,6 +48801,27 @@ snapshots:
|
|||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.5)
|
||||
|
||||
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
|
||||
|
|
@ -48778,25 +48843,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:
|
||||
|
|
@ -48818,7 +48873,7 @@ snapshots:
|
|||
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)):
|
||||
ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
enhanced-resolve: 5.18.3
|
||||
|
|
@ -48826,7 +48881,7 @@ snapshots:
|
|||
semver: 7.7.3
|
||||
source-map: 0.7.6
|
||||
typescript: 5.9.3
|
||||
webpack: 5.97.1(esbuild@0.19.12)
|
||||
webpack: 5.97.1(esbuild@0.27.0)
|
||||
|
||||
ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3):
|
||||
dependencies:
|
||||
|
|
@ -49999,6 +50054,38 @@ snapshots:
|
|||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.100.2(esbuild@0.19.12):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
'@types/json-schema': 7.0.15
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(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: 4.3.3
|
||||
tapable: 2.3.0
|
||||
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:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.100.2(esbuild@0.27.0):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
|
|
@ -50061,36 +50148,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
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ export default defineConfig({
|
|||
schema: './src/db/schema/index.ts',
|
||||
out: './src/db/migrations',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || 'postgresql://manacore:password@localhost:5432/manacore',
|
||||
url: process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/manacore',
|
||||
},
|
||||
schemaFilter: ['auth', 'credits', 'public'],
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.3",
|
||||
"helmet": "^8.0.0",
|
||||
"jose": "^6.1.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"postgres": "^3.4.5",
|
||||
|
|
|
|||
|
|
@ -126,6 +126,17 @@ export class AuthController {
|
|||
return this.betterAuthService.validateToken(body.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWKS (JSON Web Key Set)
|
||||
*
|
||||
* Returns public keys for JWT verification.
|
||||
* This is a passthrough to Better Auth's JWKS.
|
||||
*/
|
||||
@Get('jwks')
|
||||
async getJwks() {
|
||||
return this.betterAuthService.getJwks();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// B2B Registration
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
* This file configures Better Auth with:
|
||||
* - Email/password authentication
|
||||
* - Organization plugin for B2B (multi-tenant)
|
||||
* - JWT plugin with custom claims (credit_balance, customer_type, organization)
|
||||
* - JWT plugin with minimal claims
|
||||
* - Drizzle adapter for PostgreSQL
|
||||
*
|
||||
* ARCHITECTURE DECISION (2024-12):
|
||||
* We use MINIMAL JWT claims. Organization and credit data should be fetched
|
||||
* via API calls, not embedded in JWTs. See docs/AUTHENTICATION_ARCHITECTURE.md
|
||||
*
|
||||
* @see https://www.better-auth.com/docs
|
||||
* @see BETTER_AUTH_FINAL_PLAN.md
|
||||
*/
|
||||
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
|
@ -16,16 +19,32 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|||
import { jwt } from 'better-auth/plugins/jwt';
|
||||
import { organization } from 'better-auth/plugins/organization';
|
||||
import { getDb } from '../db/connection';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { balances } from '../db/schema/credits.schema';
|
||||
import { organizations, members } from '../db/schema/organizations.schema';
|
||||
import {
|
||||
organizations,
|
||||
members,
|
||||
invitations,
|
||||
} from '../db/schema/organizations.schema';
|
||||
import {
|
||||
users,
|
||||
sessions,
|
||||
accounts,
|
||||
verificationTokens,
|
||||
jwks,
|
||||
} from '../db/schema/auth.schema';
|
||||
import type { JWTPayloadContext } from './types/better-auth.types';
|
||||
|
||||
/**
|
||||
* JWT Custom Payload Interface
|
||||
*
|
||||
* Defines the structure of custom claims included in JWT tokens.
|
||||
* These claims are added to the standard JWT payload (sub, iat, exp, etc.)
|
||||
* MINIMAL claims only. Organization context and credits are available via:
|
||||
* - GET /organization/get-active-member - org membership & role
|
||||
* - GET /api/v1/credits/balance - credit balance
|
||||
*
|
||||
* Why minimal claims?
|
||||
* 1. Credit balance changes frequently - JWT would be stale
|
||||
* 2. Organization context available via Better Auth org plugin APIs
|
||||
* 3. Smaller tokens = better performance
|
||||
* 4. Follows Better Auth's session-based design
|
||||
*/
|
||||
export interface JWTCustomPayload {
|
||||
/** User ID (standard JWT claim) */
|
||||
|
|
@ -37,145 +56,13 @@ export interface JWTCustomPayload {
|
|||
/** User role (user, admin, service) */
|
||||
role: string;
|
||||
|
||||
/** Customer type: B2C (individual) or B2B (organization member) */
|
||||
customer_type: 'b2c' | 'b2b';
|
||||
|
||||
/** Organization context (null for B2C users) */
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
role: 'owner' | 'admin' | 'member';
|
||||
} | null;
|
||||
|
||||
/** User's credit balance (personal for B2C, allocated for B2B) */
|
||||
credit_balance: number;
|
||||
|
||||
/** Application ID (memoro, chat, picture, etc.) */
|
||||
app_id?: string;
|
||||
|
||||
/** Device ID (for mobile apps) */
|
||||
device_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get personal credit balance (B2C users)
|
||||
*
|
||||
* @param userId - User ID
|
||||
* @param databaseUrl - Database connection URL
|
||||
* @returns Credit balance or 0 if not found
|
||||
*/
|
||||
async function getPersonalCreditBalance(userId: string, databaseUrl: string): Promise<number> {
|
||||
try {
|
||||
const db = getDb(databaseUrl);
|
||||
|
||||
const [balance] = await db
|
||||
.select({ balance: balances.balance })
|
||||
.from(balances)
|
||||
.where(eq(balances.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
return balance?.balance ?? 0;
|
||||
} catch (error) {
|
||||
console.error('Error fetching personal credit balance:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get employee credit balance (B2B users)
|
||||
*
|
||||
* For B2B employees, this returns their allocated credit balance.
|
||||
* The balance is stored in the same balances table but tracked separately per employee.
|
||||
*
|
||||
* @param userId - Employee user ID
|
||||
* @param organizationId - Organization ID
|
||||
* @param databaseUrl - Database connection URL
|
||||
* @returns Allocated credit balance or 0 if not found
|
||||
*/
|
||||
async function getEmployeeCreditBalance(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
databaseUrl: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
const db = getDb(databaseUrl);
|
||||
|
||||
// Get employee's personal balance (which represents their allocated credits from the org)
|
||||
const [balance] = await db
|
||||
.select({ balance: balances.balance })
|
||||
.from(balances)
|
||||
.where(eq(balances.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
return balance?.balance ?? 0;
|
||||
} catch (error) {
|
||||
console.error('Error fetching employee credit balance:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get organization membership data
|
||||
*
|
||||
* Queries the organization and member tables to get:
|
||||
* - Organization name
|
||||
* - User's role in the organization
|
||||
*
|
||||
* @param userId - User ID
|
||||
* @param organizationId - Organization ID
|
||||
* @param databaseUrl - Database connection URL
|
||||
* @returns Organization data with name and role, or null if not found
|
||||
*/
|
||||
async function getOrganizationMembership(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
databaseUrl: string
|
||||
): Promise<{ name: string; role: 'owner' | 'admin' | 'member' } | null> {
|
||||
try {
|
||||
const db = getDb(databaseUrl);
|
||||
|
||||
// Query member table to get user's role in the organization
|
||||
const [memberRecord] = await db
|
||||
.select({
|
||||
role: members.role,
|
||||
})
|
||||
.from(members)
|
||||
.where(and(eq(members.userId, userId), eq(members.organizationId, organizationId)))
|
||||
.limit(1);
|
||||
|
||||
if (!memberRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Query organization table to get organization name
|
||||
const [orgRecord] = await db
|
||||
.select({
|
||||
name: organizations.name,
|
||||
})
|
||||
.from(organizations)
|
||||
.where(eq(organizations.id, organizationId))
|
||||
.limit(1);
|
||||
|
||||
if (!orgRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: orgRecord.name,
|
||||
role: memberRecord.role as 'owner' | 'admin' | 'member',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching organization membership:', error);
|
||||
return null;
|
||||
}
|
||||
/** Session ID for reference */
|
||||
sid: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Better Auth instance
|
||||
*
|
||||
* This function initializes Better Auth with the database connection URL.
|
||||
* It must be called with the database URL from the configuration.
|
||||
*
|
||||
* @param databaseUrl - PostgreSQL connection URL
|
||||
* @returns Better Auth instance
|
||||
*/
|
||||
|
|
@ -187,16 +74,19 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
database: drizzleAdapter(db, {
|
||||
provider: 'pg',
|
||||
schema: {
|
||||
// Auth tables
|
||||
user: 'auth.users',
|
||||
session: 'auth.sessions',
|
||||
account: 'auth.accounts',
|
||||
verification: 'auth.verification_tokens',
|
||||
// Auth tables (actual Drizzle table objects)
|
||||
user: users,
|
||||
session: sessions,
|
||||
account: accounts,
|
||||
verification: verificationTokens,
|
||||
|
||||
// Organization tables (Better Auth creates these schemas)
|
||||
organization: 'auth.organizations',
|
||||
member: 'auth.members',
|
||||
invitation: 'auth.invitations',
|
||||
// Organization tables
|
||||
organization: organizations,
|
||||
member: members,
|
||||
invitation: invitations,
|
||||
|
||||
// JWT plugin table
|
||||
jwks: jwks,
|
||||
},
|
||||
}),
|
||||
|
||||
|
|
@ -226,7 +116,12 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
* - Create/update/delete organizations
|
||||
* - Invite/add/remove members
|
||||
* - Role-based access control
|
||||
* - Email-based invitations
|
||||
* - Active organization tracking (session.activeOrganizationId)
|
||||
*
|
||||
* Client apps use these endpoints for org context:
|
||||
* - GET /organization/get-active-member
|
||||
* - GET /organization/get-active-member-role
|
||||
* - POST /organization/set-active
|
||||
*/
|
||||
organization({
|
||||
// Allow users to create their own organizations
|
||||
|
|
@ -242,22 +137,10 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
organization: organization.name,
|
||||
invitationId: data.id,
|
||||
});
|
||||
|
||||
// Example email template:
|
||||
// Subject: Join ${organization.name} on Mana Universe
|
||||
// Body: You've been invited to join ${organization.name}
|
||||
// Click here to accept: ${baseURL}/invite/${data.id}
|
||||
},
|
||||
|
||||
// Custom roles and permissions
|
||||
organizationRole: {
|
||||
/**
|
||||
* Owner Role
|
||||
* - Full organization control
|
||||
* - Can delete organization
|
||||
* - Can manage all members
|
||||
* - Can allocate credits to employees
|
||||
*/
|
||||
owner: {
|
||||
permissions: [
|
||||
'organization:update',
|
||||
|
|
@ -265,17 +148,10 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
'members:invite',
|
||||
'members:remove',
|
||||
'members:update_role',
|
||||
'credits:allocate', // Custom permission
|
||||
'credits:view_all', // Custom permission
|
||||
'credits:allocate',
|
||||
'credits:view_all',
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* Admin Role
|
||||
* - Can update organization settings
|
||||
* - Can invite and remove members
|
||||
* - Can view all credit usage
|
||||
*/
|
||||
admin: {
|
||||
permissions: [
|
||||
'organization:update',
|
||||
|
|
@ -284,12 +160,6 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
'credits:view_all',
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* Member Role
|
||||
* - Basic organization access
|
||||
* - Can only view their own credits
|
||||
*/
|
||||
member: {
|
||||
permissions: ['credits:view_own'],
|
||||
},
|
||||
|
|
@ -299,99 +169,35 @@ export function createBetterAuth(databaseUrl: string) {
|
|||
/**
|
||||
* JWT Plugin
|
||||
*
|
||||
* Generates JWT tokens with custom claims for:
|
||||
* - Credit balance
|
||||
* - Customer type (B2C vs B2B)
|
||||
* - Organization context
|
||||
* - App/device metadata
|
||||
* Generates JWT tokens with MINIMAL claims.
|
||||
*
|
||||
* DO NOT add complex claims like:
|
||||
* - credit_balance (stale after 15min, fetch via API instead)
|
||||
* - organization details (use Better Auth org plugin APIs)
|
||||
* - customer_type (derive from activeOrganizationId presence)
|
||||
*
|
||||
* Apps should call APIs for dynamic data:
|
||||
* - Credits: GET /api/v1/credits/balance
|
||||
* - Org info: GET /organization/get-active-member
|
||||
*/
|
||||
jwt({
|
||||
jwt: {
|
||||
issuer: 'mana-core',
|
||||
issuer: process.env.JWT_ISSUER || 'manacore',
|
||||
audience: process.env.JWT_AUDIENCE || 'manacore',
|
||||
expirationTime: '15m', // 15 minutes for access tokens
|
||||
expirationTime: '15m',
|
||||
|
||||
/**
|
||||
* Define custom JWT payload
|
||||
* Define minimal JWT payload
|
||||
*
|
||||
* This function is called when generating a JWT token.
|
||||
* It enriches the standard JWT claims with custom data.
|
||||
*
|
||||
* @param context - JWT payload context with user and session
|
||||
* @returns Custom JWT payload
|
||||
* Only includes static user info that doesn't change frequently.
|
||||
*/
|
||||
async definePayload({ user, session }: JWTPayloadContext) {
|
||||
// Get user's active organization (from session metadata or first membership)
|
||||
const activeOrgId = session.activeOrganizationId;
|
||||
|
||||
let organizationData: JWTCustomPayload['organization'] = null;
|
||||
let creditBalance = 0;
|
||||
let customerType: 'b2c' | 'b2b' = 'b2c';
|
||||
|
||||
if (activeOrgId) {
|
||||
// B2B user - get organization membership from database
|
||||
try {
|
||||
// Query actual organization and membership data
|
||||
const membership = await getOrganizationMembership(
|
||||
user.id,
|
||||
activeOrgId,
|
||||
databaseUrl
|
||||
);
|
||||
|
||||
if (membership) {
|
||||
// Get employee's allocated credit balance
|
||||
creditBalance = await getEmployeeCreditBalance(
|
||||
user.id,
|
||||
activeOrgId,
|
||||
databaseUrl
|
||||
);
|
||||
|
||||
organizationData = {
|
||||
id: activeOrgId,
|
||||
name: membership.name,
|
||||
role: membership.role,
|
||||
};
|
||||
|
||||
customerType = 'b2b';
|
||||
} else {
|
||||
// User is not a member of this organization, fall back to B2C
|
||||
console.warn(
|
||||
`User ${user.id} is not a member of organization ${activeOrgId}`
|
||||
);
|
||||
creditBalance = await getPersonalCreditBalance(user.id, databaseUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching organization data:', error);
|
||||
// Fall back to B2C on error
|
||||
creditBalance = await getPersonalCreditBalance(user.id, databaseUrl);
|
||||
}
|
||||
} else {
|
||||
// B2C user - get personal credit balance
|
||||
creditBalance = await getPersonalCreditBalance(user.id, databaseUrl);
|
||||
}
|
||||
|
||||
// Build custom JWT payload
|
||||
const payload: Partial<JWTCustomPayload> = {
|
||||
// Standard claims
|
||||
definePayload({ user, session }: JWTPayloadContext) {
|
||||
return {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: user.role || 'user',
|
||||
|
||||
// Customer type
|
||||
customer_type: customerType,
|
||||
|
||||
// Organization (null for B2C)
|
||||
organization: organizationData,
|
||||
|
||||
// Credits
|
||||
credit_balance: creditBalance,
|
||||
|
||||
// App metadata (from session)
|
||||
app_id: (session.metadata?.appId as string) || undefined,
|
||||
device_id: (session.metadata?.deviceId as string) || undefined,
|
||||
role: (user as { role?: string }).role || 'user',
|
||||
sid: session.id,
|
||||
};
|
||||
|
||||
return payload;
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import type {
|
|||
BetterAuthSession,
|
||||
} from '../types/better-auth.types';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
||||
|
||||
// Re-export DTOs and result types for external use
|
||||
export type {
|
||||
|
|
@ -418,6 +419,73 @@ export class BetterAuthService {
|
|||
|
||||
const { user } = result;
|
||||
|
||||
// Get session token (used as refresh token)
|
||||
const session = hasSession(result) ? result.session : null;
|
||||
const sessionToken = session?.token || (hasToken(result) ? result.token : '');
|
||||
|
||||
// Generate JWT access token using Better Auth's JWT plugin
|
||||
let accessToken = '';
|
||||
try {
|
||||
const api = this.auth.api as any;
|
||||
|
||||
// Use Better Auth's signJWT with the jwks table
|
||||
const jwtResult = await api.signJWT({
|
||||
body: {
|
||||
payload: {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: (user as BetterAuthUser).role || 'user',
|
||||
sid: session?.id || '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
accessToken = jwtResult?.token || '';
|
||||
|
||||
// Fallback to manual JWT if Better Auth fails
|
||||
if (!accessToken) {
|
||||
throw new Error('Better Auth signJWT returned empty token');
|
||||
}
|
||||
} catch (jwtError) {
|
||||
console.warn('[signIn] Better Auth signJWT failed, using manual JWT generation:', jwtError);
|
||||
|
||||
// Fallback: Generate JWT manually using jsonwebtoken
|
||||
const privateKey = this.configService.get<string>('jwt.privateKey');
|
||||
const issuer = this.configService.get<string>('jwt.issuer') || 'manacore';
|
||||
const audience = this.configService.get<string>('jwt.audience') || 'manacore';
|
||||
|
||||
console.log('[signIn] Private key exists:', !!privateKey);
|
||||
console.log('[signIn] Private key length:', privateKey?.length);
|
||||
console.log('[signIn] Private key starts with:', privateKey?.substring(0, 30));
|
||||
console.log('[signIn] Issuer:', issuer);
|
||||
console.log('[signIn] Audience:', audience);
|
||||
|
||||
if (privateKey) {
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: (user as BetterAuthUser).role || 'user',
|
||||
sid: session?.id || '',
|
||||
};
|
||||
|
||||
accessToken = jwt.sign(payload, privateKey, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '15m',
|
||||
issuer,
|
||||
audience,
|
||||
});
|
||||
|
||||
console.log('[signIn] Generated JWT (first 50 chars):', accessToken?.substring(0, 50));
|
||||
// Decode to verify
|
||||
const decoded = jwt.decode(accessToken, { complete: true });
|
||||
console.log('[signIn] Generated JWT header:', decoded?.header);
|
||||
console.log('[signIn] Generated JWT payload:', decoded?.payload);
|
||||
} else {
|
||||
console.error('[signIn] No JWT private key configured');
|
||||
accessToken = sessionToken;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
|
|
@ -425,7 +493,9 @@ export class BetterAuthService {
|
|||
name: user.name,
|
||||
role: (user as BetterAuthUser).role,
|
||||
},
|
||||
token: hasToken(result) ? result.token : '',
|
||||
accessToken,
|
||||
refreshToken: sessionToken,
|
||||
expiresIn: 15 * 60, // 15 minutes in seconds
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
|
|
@ -617,7 +687,7 @@ export class BetterAuthService {
|
|||
}
|
||||
|
||||
// Check if refresh token is expired
|
||||
if (new Date() > session.refreshTokenExpiresAt) {
|
||||
if (!session.refreshTokenExpiresAt || new Date() > session.refreshTokenExpiresAt) {
|
||||
throw new UnauthorizedException('Refresh token expired');
|
||||
}
|
||||
|
||||
|
|
@ -715,26 +785,44 @@ export class BetterAuthService {
|
|||
*/
|
||||
async validateToken(token: string): Promise<ValidateTokenResult> {
|
||||
try {
|
||||
const publicKey = this.configService.get<string>('jwt.publicKey');
|
||||
if (!publicKey) {
|
||||
throw new Error('JWT public key not configured');
|
||||
}
|
||||
console.log('[validateToken] Token (first 50 chars):', token?.substring(0, 50));
|
||||
|
||||
const audience = this.configService.get<string>('jwt.audience');
|
||||
const issuer = this.configService.get<string>('jwt.issuer');
|
||||
// Decode to check the algorithm
|
||||
const decoded = jwt.decode(token, { complete: true });
|
||||
console.log('[validateToken] Decoded header:', decoded?.header);
|
||||
|
||||
const payload = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
audience,
|
||||
// Use our JWKS endpoint (NestJS prefix: /api/v1)
|
||||
const baseUrl = this.configService.get<string>('BASE_URL') || 'http://localhost:3001';
|
||||
const jwksUrl = new URL('/api/v1/auth/jwks', baseUrl);
|
||||
|
||||
console.log('[validateToken] Using JWKS from:', jwksUrl.toString());
|
||||
|
||||
// Create JWKS fetcher
|
||||
const JWKS = createRemoteJWKSet(jwksUrl);
|
||||
|
||||
// Get issuer/audience from config (Better Auth uses BASE_URL by default)
|
||||
const issuer = this.configService.get<string>('jwt.issuer') || baseUrl;
|
||||
const audience = this.configService.get<string>('jwt.audience') || baseUrl;
|
||||
|
||||
console.log('[validateToken] Issuer:', issuer);
|
||||
console.log('[validateToken] Audience:', audience);
|
||||
|
||||
// Verify using jose library with Better Auth's JWKS
|
||||
const { payload } = await jwtVerify(token, JWKS, {
|
||||
issuer,
|
||||
}) as TokenPayload;
|
||||
audience,
|
||||
});
|
||||
|
||||
console.log('[validateToken] Verification SUCCESS');
|
||||
console.log('[validateToken] Payload:', payload);
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
payload,
|
||||
payload: payload as unknown as TokenPayload,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('[validateToken] Verification FAILED:', errorMessage);
|
||||
return {
|
||||
valid: false,
|
||||
error: errorMessage,
|
||||
|
|
@ -742,6 +830,46 @@ export class BetterAuthService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWKS (JSON Web Key Set)
|
||||
*
|
||||
* Returns public keys for JWT verification.
|
||||
* Proxies to Better Auth's internal JWKS.
|
||||
*
|
||||
* @returns JWKS with public keys
|
||||
*/
|
||||
async getJwks(): Promise<{ keys: unknown[] }> {
|
||||
try {
|
||||
// Better Auth exposes JWKS via auth.api
|
||||
const api = this.auth.api as any;
|
||||
|
||||
// Try to get JWKS from Better Auth
|
||||
if (api.getJwks) {
|
||||
const result = await api.getJwks();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback: read from jwks table directly
|
||||
const db = getDb(this.databaseUrl);
|
||||
const { jwks } = await import('../../db/schema/auth.schema');
|
||||
const keys = await db.select().from(jwks);
|
||||
|
||||
// Convert to JWKS format (EdDSA public keys)
|
||||
return {
|
||||
keys: keys.map((key) => {
|
||||
try {
|
||||
return JSON.parse(key.publicKey);
|
||||
} catch {
|
||||
return { kid: key.id, publicKey: key.publicKey };
|
||||
}
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getJwks] Error:', error);
|
||||
return { keys: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private Helper Methods
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -441,9 +441,9 @@ export interface SignInResult {
|
|||
name: string | null;
|
||||
role?: string;
|
||||
};
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
expiresIn?: number;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ export default () => ({
|
|||
},
|
||||
|
||||
jwt: {
|
||||
publicKey: process.env.JWT_PUBLIC_KEY || '',
|
||||
privateKey: process.env.JWT_PRIVATE_KEY || '',
|
||||
// Convert \n string literals to actual newlines for PEM format
|
||||
publicKey: (process.env.JWT_PUBLIC_KEY || '').replace(/\\n/g, '\n'),
|
||||
privateKey: (process.env.JWT_PRIVATE_KEY || '').replace(/\\n/g, '\n'),
|
||||
accessTokenExpiry: process.env.JWT_ACCESS_TOKEN_EXPIRY || '15m',
|
||||
refreshTokenExpiry: process.env.JWT_REFRESH_TOKEN_EXPIRY || '7d',
|
||||
issuer: process.env.JWT_ISSUER || 'manacore',
|
||||
|
|
|
|||
|
|
@ -1,78 +1,83 @@
|
|||
import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum, index } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const authSchema = pgSchema('auth');
|
||||
|
||||
// Enum for user roles
|
||||
export const userRoleEnum = pgEnum('user_role', ['user', 'admin', 'service']);
|
||||
|
||||
// Users table
|
||||
// Users table (Better Auth schema)
|
||||
export const users = authSchema.table('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
name: text('name').notNull(),
|
||||
email: text('email').unique().notNull(),
|
||||
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||
name: text('name'),
|
||||
avatarUrl: text('avatar_url'),
|
||||
role: userRoleEnum('role').default('user').notNull(),
|
||||
image: text('image'), // Better Auth uses 'image' not 'avatarUrl'
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
// Custom fields (not required by Better Auth)
|
||||
role: userRoleEnum('role').default('user').notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
||||
});
|
||||
|
||||
// Sessions table
|
||||
// Sessions table (Better Auth schema)
|
||||
export const sessions = authSchema.table('sessions', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
token: text('token').unique().notNull(),
|
||||
refreshToken: text('refresh_token').unique().notNull(),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
// Custom fields (not required by Better Auth)
|
||||
refreshToken: text('refresh_token').unique(),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
deviceId: text('device_id'),
|
||||
deviceName: text('device_name'),
|
||||
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }).defaultNow(),
|
||||
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
||||
});
|
||||
|
||||
// Accounts table (for OAuth providers)
|
||||
// Accounts table (for OAuth providers and credentials - Better Auth schema)
|
||||
export const accounts = authSchema.table('accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
accountId: text('account_id').notNull(), // Better Auth field
|
||||
providerId: text('provider_id').notNull(), // Better Auth field (was 'provider')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
provider: text('provider').notNull(), // 'google', 'github', 'apple', etc.
|
||||
providerAccountId: text('provider_account_id').notNull(),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||
tokenType: text('token_type'),
|
||||
scope: text('scope'),
|
||||
idToken: text('id_token'),
|
||||
metadata: jsonb('metadata'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
scope: text('scope'),
|
||||
password: text('password'), // Better Auth stores hashed password here for credential provider
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Verification tokens (for email verification, password reset)
|
||||
export const verificationTokens = authSchema.table('verification_tokens', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
token: text('token').unique().notNull(),
|
||||
type: text('type').notNull(), // 'email_verification', 'password_reset'
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
usedAt: timestamp('used_at', { withTimezone: true }),
|
||||
});
|
||||
// Verification table (Better Auth schema - for email verification, password reset)
|
||||
export const verificationTokens = authSchema.table(
|
||||
'verification',
|
||||
{
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
identifier: text('identifier').notNull(), // Better Auth uses identifier (e.g., email)
|
||||
value: text('value').notNull(), // Better Auth uses value (the token)
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
identifierIdx: index('verification_identifier_idx').on(table.identifier),
|
||||
})
|
||||
);
|
||||
|
||||
// Password table (separate for security)
|
||||
export const passwords = authSchema.table('passwords', {
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
hashedPassword: text('hashed_password').notNull(),
|
||||
|
|
@ -82,23 +87,31 @@ export const passwords = authSchema.table('passwords', {
|
|||
|
||||
// Two-factor authentication
|
||||
export const twoFactorAuth = authSchema.table('two_factor_auth', {
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
secret: text('secret').notNull(),
|
||||
enabled: boolean('enabled').default(false).notNull(),
|
||||
backupCodes: jsonb('backup_codes'), // Array of hashed backup codes
|
||||
backupCodes: jsonb('backup_codes'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
enabledAt: timestamp('enabled_at', { withTimezone: true }),
|
||||
});
|
||||
|
||||
// Security events log
|
||||
export const securityEvents = authSchema.table('security_events', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
eventType: text('event_type').notNull(), // 'login', 'logout', 'password_reset', 'suspicious_activity'
|
||||
id: uuid('id').primaryKey().defaultRandom(), // Our table, can keep UUID
|
||||
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
eventType: text('event_type').notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// JWKS table (Better Auth JWT plugin - stores signing keys)
|
||||
export const jwks = authSchema.table('jwks', {
|
||||
id: text('id').primaryKey(),
|
||||
publicKey: text('public_key').notNull(),
|
||||
privateKey: text('private_key').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const transactionStatusEnum = pgEnum('transaction_status', [
|
|||
|
||||
// Credit balances (one per user)
|
||||
export const balances = creditsSchema.table('balances', {
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
balance: integer('balance').default(0).notNull(),
|
||||
|
|
@ -43,7 +43,7 @@ export const balances = creditsSchema.table('balances', {
|
|||
lastDailyResetAt: timestamp('last_daily_reset_at', { withTimezone: true }).defaultNow(),
|
||||
totalEarned: integer('total_earned').default(0).notNull(),
|
||||
totalSpent: integer('total_spent').default(0).notNull(),
|
||||
version: integer('version').default(0).notNull(), // For optimistic locking
|
||||
version: integer('version').default(0).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -53,7 +53,7 @@ export const transactions = creditsSchema.table(
|
|||
'transactions',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
type: transactionTypeEnum('type').notNull(),
|
||||
|
|
@ -61,10 +61,10 @@ export const transactions = creditsSchema.table(
|
|||
amount: integer('amount').notNull(),
|
||||
balanceBefore: integer('balance_before').notNull(),
|
||||
balanceAfter: integer('balance_after').notNull(),
|
||||
appId: text('app_id').notNull(), // 'memoro', 'chat', 'picture', etc.
|
||||
appId: text('app_id').notNull(),
|
||||
description: text('description').notNull(),
|
||||
organizationId: text('organization_id').references(() => organizations.id), // NULL for B2C, set for B2B
|
||||
metadata: jsonb('metadata'), // Additional context
|
||||
organizationId: text('organization_id').references(() => organizations.id),
|
||||
metadata: jsonb('metadata'),
|
||||
idempotencyKey: text('idempotency_key').unique(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
completedAt: timestamp('completed_at', { withTimezone: true }),
|
||||
|
|
@ -83,8 +83,8 @@ export const packages = creditsSchema.table('packages', {
|
|||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
credits: integer('credits').notNull(), // Number of credits
|
||||
priceEuroCents: integer('price_euro_cents').notNull(), // Price in euro cents
|
||||
credits: integer('credits').notNull(),
|
||||
priceEuroCents: integer('price_euro_cents').notNull(),
|
||||
stripePriceId: text('stripe_price_id').unique(),
|
||||
active: boolean('active').default(true).notNull(),
|
||||
sortOrder: integer('sort_order').default(0).notNull(),
|
||||
|
|
@ -98,7 +98,7 @@ export const purchases = creditsSchema.table(
|
|||
'purchases',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
packageId: uuid('package_id').references(() => packages.id),
|
||||
|
|
@ -124,7 +124,7 @@ export const usageStats = creditsSchema.table(
|
|||
'usage_stats',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
appId: text('app_id').notNull(),
|
||||
|
|
@ -143,12 +143,12 @@ export const organizationBalances = creditsSchema.table('organization_balances',
|
|||
organizationId: text('organization_id')
|
||||
.primaryKey()
|
||||
.references(() => organizations.id, { onDelete: 'cascade' }),
|
||||
balance: integer('balance').default(0).notNull(), // Total purchased credits
|
||||
allocatedCredits: integer('allocated_credits').default(0).notNull(), // Sum of credits allocated to employees
|
||||
availableCredits: integer('available_credits').default(0).notNull(), // balance - allocated_credits
|
||||
totalPurchased: integer('total_purchased').default(0).notNull(), // Total credits ever purchased
|
||||
totalAllocated: integer('total_allocated').default(0).notNull(), // Total ever allocated
|
||||
version: integer('version').default(0).notNull(), // For optimistic locking
|
||||
balance: integer('balance').default(0).notNull(),
|
||||
allocatedCredits: integer('allocated_credits').default(0).notNull(),
|
||||
availableCredits: integer('available_credits').default(0).notNull(),
|
||||
totalPurchased: integer('total_purchased').default(0).notNull(),
|
||||
totalAllocated: integer('total_allocated').default(0).notNull(),
|
||||
version: integer('version').default(0).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -161,17 +161,17 @@ export const creditAllocations = creditsSchema.table(
|
|||
organizationId: text('organization_id')
|
||||
.references(() => organizations.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
employeeId: uuid('employee_id')
|
||||
employeeId: text('employee_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
amount: integer('amount').notNull(), // Amount allocated (can be positive or negative)
|
||||
allocatedBy: uuid('allocated_by')
|
||||
amount: integer('amount').notNull(),
|
||||
allocatedBy: text('allocated_by')
|
||||
.references(() => users.id)
|
||||
.notNull(), // Owner or admin who made the allocation
|
||||
reason: text('reason'), // Optional reason for allocation
|
||||
balanceBefore: integer('balance_before').notNull(), // Employee balance before
|
||||
balanceAfter: integer('balance_after').notNull(), // Employee balance after
|
||||
metadata: jsonb('metadata'), // Additional context
|
||||
.notNull(),
|
||||
reason: text('reason'),
|
||||
balanceBefore: integer('balance_before').notNull(),
|
||||
balanceAfter: integer('balance_after').notNull(),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue