refactored permission system and error handling

This commit is contained in:
2025-09-26 15:35:54 +02:00
parent 2cfd6248bc
commit d025819888
26 changed files with 2312 additions and 300 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

View File

@ -72,6 +72,27 @@ install:
- [tauri](https://v2.tauri.app/start/prerequisites/)
- [rust](https://v2.tauri.app/start/prerequisites/#rust)
- install webkit2gtk + GTK3
```bash
# debian/ubuntu
sudo apt update
sudo apt install \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev
```
```bash
# fedora
sudo dnf install \
webkit2gtk4.1-devel \
gtk3-devel \
libappindicator-gtk3 \
librsvg2-devel
```
- port 3003 needs to be open/free or you need to adjust it in `nuxt.config.ts` AND `src-tauri/tauri.conf.json`
```

View File

@ -19,7 +19,7 @@
"@nuxt/eslint": "1.9.0",
"@nuxt/fonts": "0.11.4",
"@nuxt/icon": "2.0.0",
"@nuxt/ui": "^3.3.2",
"@nuxt/ui": "4.0.0",
"@nuxtjs/i18n": "10.0.6",
"@pinia/nuxt": "^0.11.1",
"@tailwindcss/vite": "^4.1.10",

351
pnpm-lock.yaml generated
View File

@ -16,16 +16,16 @@ importers:
version: 1.9.0(@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/fonts':
specifier: 0.11.4
version: 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
version: 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/icon':
specifier: 2.0.0
version: 2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
'@nuxt/ui':
specifier: ^3.3.2
version: 3.3.3(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)
specifier: 4.0.0
version: 4.0.0(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)
'@nuxtjs/i18n':
specifier: 10.0.6
version: 10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))
version: 10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))
'@pinia/nuxt':
specifier: ^0.11.1
version: 0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))
@ -67,10 +67,10 @@ importers:
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
'@vueuse/nuxt':
specifier: ^13.4.0
version: 13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
version: 13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
drizzle-orm:
specifier: ^0.44.2
version: 0.44.5(@libsql/client@0.15.15)
version: 0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)
eslint:
specifier: ^9.34.0
version: 9.35.0(jiti@2.5.1)
@ -79,7 +79,7 @@ importers:
version: 7.1.0
nuxt:
specifier: ^4.0.3
version: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
version: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
nuxt-zod-i18n:
specifier: ^1.12.0
version: 1.12.1(magicast@0.3.5)
@ -138,6 +138,34 @@ importers:
packages:
'@ai-sdk/gateway@1.0.28':
resolution: {integrity: sha512-e9RKgWVDYHsd4UkKCgKQpK+nxLSDydN18yXctzgNlmf2R7BR+HqUsTKJdZT6ArSoXBWBGhyZss0cJJnpm6YVfw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.22.4
'@ai-sdk/provider-utils@3.0.9':
resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.22.4
'@ai-sdk/provider@2.0.0':
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
engines: {node: '>=18'}
'@ai-sdk/vue@2.0.51':
resolution: {integrity: sha512-pA2r/R0IMqgm7pTPfsmkXIss8g+amQ0cZfgEXq4v91FiTQedpNBcyPwHsx6rWiPV25zPGXMcXJCw+ah4TshaCw==}
engines: {node: '>=18'}
peerDependencies:
vue: ^3.3.4
zod: ^3.22.4
peerDependenciesMeta:
vue:
optional: true
zod:
optional: true
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@ -981,9 +1009,6 @@ packages:
'@nuxt/fonts@0.11.4':
resolution: {integrity: sha512-GbLavsC+9FejVwY+KU4/wonJsKhcwOZx/eo4EuV57C4osnF/AtEmev8xqI0DNlebMEhEGZbu1MGwDDDYbeR7Bw==}
'@nuxt/icon@1.15.0':
resolution: {integrity: sha512-kA0rxqr1B601zNJNcOXera8CyYcxUCEcT7dXEC7rwAz71PRCN5emf7G656eKEQgtqrD4JSj6NQqWDgrmFcf/GQ==}
'@nuxt/icon@2.0.0':
resolution: {integrity: sha512-sy8+zkKMYp+H09S0cuTteL3zPTmktqzYPpPXV9ZkLNjrQsaPH08n7s/9wjr+C/K/w2R3u18E3+P1VIQi3xaq1A==}
@ -1004,17 +1029,17 @@ packages:
engines: {node: '>=18.12.0'}
hasBin: true
'@nuxt/ui@3.3.3':
resolution: {integrity: sha512-1JS7V3FqsLQMwt6bzHYackdUtwXU/w4nRoqKLP+5WAXnsXb4nrFInLTh3wnJGsg8N6FKz2qbREimDfNuMfmKUQ==}
'@nuxt/ui@4.0.0':
resolution: {integrity: sha512-pu5FZ8NZN2YKAiExOXuM0ImjOMe3h4/CsVgm71it+1On7OmIYHeh6SGgvaSX4Ly7FibUFllZMzJ+M5jo6KAEuw==}
hasBin: true
peerDependencies:
'@inertiajs/vue3': ^2.0.7
joi: ^17.13.0
joi: ^18.0.0
superstruct: ^2.0.0
typescript: ^5.6.3
valibot: ^1.0.0
vue-router: ^4.5.0
yup: ^1.6.0
yup: ^1.7.0
zod: ^3.22.4
peerDependenciesMeta:
'@inertiajs/vue3':
@ -1045,6 +1070,10 @@ packages:
resolution: {integrity: sha512-SQqJP6NDlmaoLzs7A74cx0Q3W4Vc+JSBlu3AN0q9+Q07Nvba5osab99GJEQ+PGnjaRwBFh35braUA2hRz9bdSA==}
engines: {node: '>=20.11.1'}
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
'@oxc-minify/binding-android-arm64@0.86.0':
resolution: {integrity: sha512-jOgbDgp6A1ax9sxHPRHBxUpxIzp2VTgbZ/6HPKIVUJ7IQqKVsELKFXIOEbCDlb1rUhZZtGf53MFypXf72kR5eQ==}
engines: {node: '>=14.0.0'}
@ -2121,6 +2150,11 @@ packages:
peerDependencies:
vue: '>=3.5.18'
'@unhead/vue@2.0.17':
resolution: {integrity: sha512-jzmGZYeMAhETV6qfetmLbZzUjjx1TjdNvFSobeFZb73D7dwD9wl/nOAx36qq+TvjZsLJdF5PQWToz2oDGAUqCg==}
peerDependencies:
vue: '>=3.5.18'
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
cpu: [arm]
@ -2446,6 +2480,12 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ai@5.0.51:
resolution: {integrity: sha512-ToKW099QWUJNqePZbWGg8FSfxTxS3UN9U6yCla8rYdW0EBTDNPnpRwK1N6ER9TfV+dFtdUu+ZgKSlhQnEThriQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.22.4
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -2837,6 +2877,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@ -3294,6 +3343,10 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
@ -3383,6 +3436,20 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@ -3484,6 +3551,9 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
hey-listen@1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@ -3690,6 +3760,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@ -3944,6 +4017,18 @@ packages:
mocked-exports@0.1.1:
resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==}
motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
motion-v@1.7.1:
resolution: {integrity: sha512-B22fYcHGx05moUtoIH0ZP/JzeacGOHzLkLmMTKU9tRB+uVMSfgqiXVzZb602qiG1ap8W7TZ+5RD5R3MmODu9oA==}
peerDependencies:
'@vueuse/core': '>=10.0.0'
vue: '>=3.0.0'
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@ -4767,6 +4852,11 @@ packages:
engines: {node: '>=16'}
hasBin: true
swrv@1.1.0:
resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
peerDependencies:
vue: '>=3.2.26 < 4'
system-architecture@0.1.0:
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
engines: {node: '>=18'}
@ -4894,6 +4984,9 @@ packages:
unhead@2.0.14:
resolution: {integrity: sha512-dRP6OCqtShhMVZQe1F4wdt/WsYl2MskxKK+cvfSo0lQnrPJ4oAUQEkxRg7pPP+vJENabhlir31HwAyHUv7wfMg==}
unhead@2.0.17:
resolution: {integrity: sha512-xX3PCtxaE80khRZobyWCVxeFF88/Tg9eJDcJWY9us727nsTC7C449B8BUfVBmiF2+3LjPcmqeoB2iuMs0U4oJQ==}
unicode-properties@1.4.1:
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
@ -4911,19 +5004,19 @@ packages:
unifont@0.4.1:
resolution: {integrity: sha512-zKSY9qO8svWYns+FGKjyVdLvpGPwqmsCjeJLN1xndMiqxHWBAhoWDMYMG960MxeV48clBmG+fDP59dHY1VoZvg==}
unimport@4.2.0:
resolution: {integrity: sha512-mYVtA0nmzrysnYnyb3ALMbByJ+Maosee2+WyE0puXl+Xm2bUwPorPaaeZt0ETfuroPOtG8jj1g/qeFZ6buFnag==}
engines: {node: '>=18.12.0'}
unimport@5.2.0:
resolution: {integrity: sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==}
engines: {node: '>=18.12.0'}
unplugin-auto-import@19.3.0:
resolution: {integrity: sha512-iIi0u4Gq2uGkAOGqlPJOAMI8vocvjh1clGTfSK4SOrJKrt+tirrixo/FjgBwXQNNdS7ofcr7OxzmOb/RjWxeEQ==}
unimport@5.4.0:
resolution: {integrity: sha512-g/OLFZR2mEfqbC6NC9b2225eCJGvufxq34mj6kM3OmI5gdSL0qyqtnv+9qmsGpAmnzSl6x0IWZj4W+8j2hLkMA==}
engines: {node: '>=18.12.0'}
unplugin-auto-import@20.2.0:
resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==}
engines: {node: '>=14'}
peerDependencies:
'@nuxt/kit': ^3.2.2
'@nuxt/kit': ^4.0.0
'@vueuse/core': '*'
peerDependenciesMeta:
'@nuxt/kit':
@ -4939,8 +5032,8 @@ packages:
resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==}
engines: {node: '>=20.19.0'}
unplugin-vue-components@28.8.0:
resolution: {integrity: sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==}
unplugin-vue-components@29.1.0:
resolution: {integrity: sha512-z/9ACPXth199s9aCTCdKZAhe5QGOpvzJYP+Hkd0GN1/PpAmsu+W3UlRY3BJAewPqQxh5xi56+Og6mfiCV1Jzpg==}
engines: {node: '>=14'}
peerDependencies:
'@babel/parser': ^7.15.8
@ -5226,8 +5319,8 @@ packages:
vue-bundle-renderer@2.1.2:
resolution: {integrity: sha512-M4WRBO/O/7G9phGaGH9AOwOnYtY9ZpPoDVpBpRzR2jO5rFL9mgIlQIgums2ljCTC2HL1jDXFQc//CzWcAQHgAw==}
vue-component-type-helpers@3.0.6:
resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==}
vue-component-type-helpers@3.0.8:
resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@ -5388,6 +5481,32 @@ packages:
snapshots:
'@ai-sdk/gateway@1.0.28(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/provider-utils@3.0.9(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@standard-schema/spec': 1.0.0
eventsource-parser: 3.0.6
zod: 3.25.76
'@ai-sdk/provider@2.0.0':
dependencies:
json-schema: 0.4.0
'@ai-sdk/vue@2.0.51(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)':
dependencies:
'@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
ai: 5.0.51(zod@3.25.76)
swrv: 1.1.0(vue@3.5.21(typescript@5.9.2))
optionalDependencies:
vue: 3.5.21(typescript@5.9.2)
zod: 3.25.76
'@alloc/quick-lru@5.2.0': {}
'@antfu/install-pkg@1.1.0':
@ -6345,7 +6464,7 @@ snapshots:
- utf-8-validate
- vite
'@nuxt/fonts@0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))':
'@nuxt/fonts@0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/kit': 3.19.1(magicast@0.3.5)
@ -6366,7 +6485,7 @@ snapshots:
ufo: 1.6.1
unifont: 0.4.1
unplugin: 2.3.10
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@ -6391,28 +6510,6 @@ snapshots:
- uploadthing
- vite
'@nuxt/icon@1.15.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@iconify/collections': 1.0.592
'@iconify/types': 2.0.0
'@iconify/utils': 2.3.0
'@iconify/vue': 5.0.0(vue@3.5.21(typescript@5.9.2))
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/kit': 3.19.1(magicast@0.3.5)
consola: 3.4.2
local-pkg: 1.1.2
mlly: 1.8.0
ohash: 2.0.11
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.9.0
tinyglobby: 0.2.15
transitivePeerDependencies:
- magicast
- supports-color
- vite
- vue
'@nuxt/icon@2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@iconify/collections': 1.0.592
@ -6516,13 +6613,14 @@ snapshots:
transitivePeerDependencies:
- magicast
'@nuxt/ui@3.3.3(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)':
'@nuxt/ui@4.0.0(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)':
dependencies:
'@ai-sdk/vue': 2.0.51(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)
'@iconify/vue': 5.0.0(vue@3.5.21(typescript@5.9.2))
'@internationalized/date': 3.9.0
'@internationalized/number': 3.6.5
'@nuxt/fonts': 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/icon': 1.15.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
'@nuxt/fonts': 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/icon': 2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
'@nuxt/kit': 4.1.1(magicast@0.3.5)
'@nuxt/schema': 4.1.1
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
@ -6530,7 +6628,7 @@ snapshots:
'@tailwindcss/postcss': 4.1.13
'@tailwindcss/vite': 4.1.13(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
'@tanstack/vue-table': 8.21.3(vue@3.5.21(typescript@5.9.2))
'@unhead/vue': 2.0.14(vue@3.5.21(typescript@5.9.2))
'@unhead/vue': 2.0.17(vue@3.5.21(typescript@5.9.2))
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
'@vueuse/integrations': 13.9.0(change-case@5.4.4)(fuse.js@7.1.0)(vue@3.5.21(typescript@5.9.2))
colortranslator: 5.0.0
@ -6548,6 +6646,7 @@ snapshots:
knitwork: 1.2.0
magic-string: 0.30.19
mlly: 1.8.0
motion-v: 1.7.1(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
ohash: 2.0.11
pathe: 2.0.3
reka-ui: 2.5.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
@ -6558,10 +6657,10 @@ snapshots:
tinyglobby: 0.2.15
typescript: 5.9.2
unplugin: 2.3.10
unplugin-auto-import: 19.3.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))
unplugin-vue-components: 28.8.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2))
unplugin-auto-import: 20.2.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))
unplugin-vue-components: 29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2))
vaul-vue: 0.4.1(reka-ui@2.5.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
vue-component-type-helpers: 3.0.6
vue-component-type-helpers: 3.0.8
optionalDependencies:
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
zod: 3.25.76
@ -6575,6 +6674,7 @@ snapshots:
- '@babel/parser'
- '@capacitor/preferences'
- '@deno/kv'
- '@emotion/is-prop-valid'
- '@netlify/blobs'
- '@planetscale/database'
- '@upstash/redis'
@ -6597,6 +6697,8 @@ snapshots:
- magicast
- nprogress
- qrcode
- react
- react-dom
- sortablejs
- supports-color
- universal-cookie
@ -6670,7 +6772,7 @@ snapshots:
transitivePeerDependencies:
- magicast
'@nuxtjs/i18n@10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))':
'@nuxtjs/i18n@10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@intlify/core': 11.1.12
'@intlify/h3': 0.7.1
@ -6697,7 +6799,7 @@ snapshots:
ufo: 1.6.1
unplugin: 2.3.10
unplugin-vue-router: 0.14.0(@vue/compiler-sfc@3.5.21)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
vue-i18n: 11.1.12(vue@3.5.21(typescript@5.9.2))
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
transitivePeerDependencies:
@ -6728,6 +6830,8 @@ snapshots:
- uploadthing
- vue
'@opentelemetry/api@1.9.0': {}
'@oxc-minify/binding-android-arm64@0.86.0':
optional: true
@ -7516,6 +7620,12 @@ snapshots:
unhead: 2.0.14
vue: 3.5.21(typescript@5.9.2)
'@unhead/vue@2.0.17(vue@3.5.21(typescript@5.9.2))':
dependencies:
hookable: 5.5.3
unhead: 2.0.17
vue: 3.5.21(typescript@5.9.2)
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
optional: true
@ -7830,13 +7940,13 @@ snapshots:
'@vueuse/metadata@13.9.0': {}
'@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
'@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
dependencies:
'@nuxt/kit': 3.19.1(magicast@0.3.5)
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
'@vueuse/metadata': 13.9.0
local-pkg: 1.1.2
nuxt: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
nuxt: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
vue: 3.5.21(typescript@5.9.2)
transitivePeerDependencies:
- magicast
@ -7876,6 +7986,14 @@ snapshots:
agent-base@7.1.4: {}
ai@5.0.51(zod@3.25.76):
dependencies:
'@ai-sdk/gateway': 1.0.28(zod@3.25.76)
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
'@opentelemetry/api': 1.9.0
zod: 3.25.76
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@ -8257,10 +8375,10 @@ snapshots:
data-uri-to-buffer@4.0.1: {}
db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)):
db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)):
optionalDependencies:
'@libsql/client': 0.15.15
drizzle-orm: 0.44.5(@libsql/client@0.15.15)
drizzle-orm: 0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)
de-indent@1.0.2: {}
@ -8268,6 +8386,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
@ -8338,9 +8460,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.44.5(@libsql/client@0.15.15):
drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0):
optionalDependencies:
'@libsql/client': 0.15.15
'@opentelemetry/api': 1.9.0
duplexer@0.1.2: {}
@ -8692,6 +8815,8 @@ snapshots:
events@3.3.0: {}
eventsource-parser@3.0.6: {}
execa@8.0.1:
dependencies:
cross-spawn: 7.0.6
@ -8803,6 +8928,12 @@ snapshots:
fraction.js@4.3.7: {}
framer-motion@12.23.12:
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
fresh@2.0.0: {}
fsevents@2.3.3:
@ -8906,6 +9037,8 @@ snapshots:
he@1.2.0: {}
hey-listen@1.0.8: {}
hookable@5.5.3: {}
http-errors@2.0.0:
@ -9080,6 +9213,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
json-schema@0.4.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {}
@ -9322,6 +9457,24 @@ snapshots:
mocked-exports@0.1.1: {}
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
motion-v@1.7.1(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)):
dependencies:
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
framer-motion: 12.23.12
hey-listen: 1.0.8
motion-dom: 12.23.12
vue: 3.5.21(typescript@5.9.2)
transitivePeerDependencies:
- '@emotion/is-prop-valid'
- react
- react-dom
mrmime@2.0.1: {}
ms@2.1.3: {}
@ -9338,7 +9491,7 @@ snapshots:
natural-compare@1.4.0: {}
nitropack@2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)):
nitropack@2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@rollup/plugin-alias': 5.1.1(rollup@4.50.1)
@ -9359,7 +9512,7 @@ snapshots:
cookie-es: 2.0.0
croner: 9.1.0
crossws: 0.3.5
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15))
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))
defu: 6.1.4
destr: 2.0.5
dot-prop: 9.0.0
@ -9405,7 +9558,7 @@ snapshots:
unenv: 2.0.0-rc.20
unimport: 5.2.0
unplugin-utils: 0.3.0
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
untyped: 2.0.0
unwasm: 0.3.11
youch: 4.1.0-beta.8
@ -9493,7 +9646,7 @@ snapshots:
transitivePeerDependencies:
- magicast
nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1):
nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1):
dependencies:
'@nuxt/cli': 3.28.0(magicast@0.3.5)
'@nuxt/devalue': 2.0.2
@ -9528,7 +9681,7 @@ snapshots:
mlly: 1.8.0
mocked-exports: 0.1.1
nanotar: 0.2.0
nitropack: 2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15))
nitropack: 2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))
nypm: 0.6.1
ofetch: 1.4.1
ohash: 2.0.11
@ -9552,7 +9705,7 @@ snapshots:
unimport: 5.2.0
unplugin: 2.3.10
unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.21)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
untyped: 2.0.0
vue: 3.5.21(typescript@5.9.2)
vue-bundle-renderer: 2.1.2
@ -10392,6 +10545,10 @@ snapshots:
picocolors: 1.1.1
sax: 1.4.1
swrv@1.1.0(vue@3.5.21(typescript@5.9.2)):
dependencies:
vue: 3.5.21(typescript@5.9.2)
system-architecture@0.1.0: {}
tailwind-merge@3.3.1: {}
@ -10513,6 +10670,10 @@ snapshots:
dependencies:
hookable: 5.5.3
unhead@2.0.17:
dependencies:
hookable: 5.5.3
unicode-properties@1.4.1:
dependencies:
base64-js: 1.5.1
@ -10532,23 +10693,6 @@ snapshots:
css-tree: 3.1.0
ohash: 2.0.11
unimport@4.2.0:
dependencies:
acorn: 8.15.0
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
local-pkg: 1.1.2
magic-string: 0.30.19
mlly: 1.8.0
pathe: 2.0.3
picomatch: 4.0.3
pkg-types: 2.3.0
scule: 1.3.0
strip-literal: 3.0.0
tinyglobby: 0.2.15
unplugin: 2.3.10
unplugin-utils: 0.2.5
unimport@5.2.0:
dependencies:
acorn: 8.15.0
@ -10566,14 +10710,31 @@ snapshots:
unplugin: 2.3.10
unplugin-utils: 0.2.5
unplugin-auto-import@19.3.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2))):
unimport@5.4.0:
dependencies:
acorn: 8.15.0
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
local-pkg: 1.1.2
magic-string: 0.30.19
mlly: 1.8.0
pathe: 2.0.3
picomatch: 4.0.3
pkg-types: 2.3.0
scule: 1.3.0
strip-literal: 3.0.0
tinyglobby: 0.2.15
unplugin: 2.3.10
unplugin-utils: 0.3.0
unplugin-auto-import@20.2.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2))):
dependencies:
local-pkg: 1.1.2
magic-string: 0.30.19
picomatch: 4.0.3
unimport: 4.2.0
unimport: 5.4.0
unplugin: 2.3.10
unplugin-utils: 0.2.5
unplugin-utils: 0.3.0
optionalDependencies:
'@nuxt/kit': 4.1.1(magicast@0.3.5)
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
@ -10588,16 +10749,16 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.3
unplugin-vue-components@28.8.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)):
unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)):
dependencies:
chokidar: 3.6.0
debug: 4.4.1
debug: 4.4.3
local-pkg: 1.1.2
magic-string: 0.30.19
mlly: 1.8.0
tinyglobby: 0.2.15
unplugin: 2.3.10
unplugin-utils: 0.2.5
unplugin-utils: 0.3.0
vue: 3.5.21(typescript@5.9.2)
optionalDependencies:
'@babel/parser': 7.28.4
@ -10688,7 +10849,7 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
unstorage@1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0):
unstorage@1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0):
dependencies:
anymatch: 3.1.3
chokidar: 4.0.3
@ -10699,7 +10860,7 @@ snapshots:
ofetch: 1.4.1
ufo: 1.6.1
optionalDependencies:
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15))
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))
ioredis: 5.7.0
untun@0.1.3:
@ -10861,7 +11022,7 @@ snapshots:
dependencies:
ufo: 1.6.1
vue-component-type-helpers@3.0.6: {}
vue-component-type-helpers@3.0.8: {}
vue-demi@0.14.10(vue@3.5.21(typescript@5.9.2)):
dependencies:

View File

@ -67,14 +67,21 @@ impl HlcService {
/// Factory-Funktion: Erstellt und initialisiert einen neuen HLC-Service aus einer bestehenden DB-Verbindung.
/// Dies ist die bevorzugte Methode zur Instanziierung.
pub fn new_from_connection(
conn: &Connection,
app_handle: &AppHandle,
) -> Result<Self, HlcError> {
pub fn try_initialize(conn: &Connection, app_handle: &AppHandle) -> Result<Self, HlcError> {
// 1. Hole oder erstelle eine persistente Node-ID
let node_id_str = Self::get_or_create_device_id(app_handle)?;
let node_id = ID::try_from(node_id_str.as_bytes()).map_err(|e| {
// Parse den String in ein Uuid-Objekt.
let uuid = Uuid::parse_str(&node_id_str).map_err(|e| {
HlcError::ParseNodeId(format!(
"Stored device ID is not a valid UUID: {}. Error: {}",
node_id_str, e
))
})?;
// Hol dir die rohen 16 Bytes und erstelle daraus die uhlc::ID.
// Das `*` dereferenziert den `&[u8; 16]` zu `[u8; 16]`, was `try_from` erwartet.
let node_id = ID::try_from(*uuid.as_bytes()).map_err(|e| {
HlcError::ParseNodeId(format!("Invalid node ID format from device store: {:?}", e))
})?;
@ -112,6 +119,7 @@ impl HlcService {
if let Some(s) = value.as_str() {
// Das ist unser Erfolgsfall. Wir haben einen &str und können
// eine Kopie davon zurückgeben.
println!("Gefundene und validierte Geräte-ID: {}", s);
if Uuid::parse_str(s).is_ok() {
// Erfolgsfall: Der Wert ist ein String UND eine gültige UUID.
// Wir können die Funktion direkt mit dem Wert verlassen.

View File

@ -115,12 +115,8 @@ impl CrdtTransformer {
Statement::Query(query) => self.transform_query_recursive(query),
// Fange alle anderen Fälle ab und gib einen Fehler zurück
_ => Err(DatabaseError::UnsupportedStatement {
statement_type: format!("{:?}", stmt)
.split('(')
.next()
.unwrap_or("")
.to_string(),
description: "This operation only accepts SELECT statements.".to_string(),
sql: stmt.to_string(),
reason: "This operation only accepts SELECT statements.".to_string(),
}),
}
}
@ -168,8 +164,8 @@ impl CrdtTransformer {
Ok(None)
} else {
Err(DatabaseError::UnsupportedStatement {
statement_type: "DELETE".to_string(),
description: "DELETE from non-table source or multiple tables".to_string(),
sql: del_stmt.to_string(),
reason: "DELETE from non-table source or multiple tables".to_string(),
})
}
}
@ -711,15 +707,15 @@ impl CrdtTransformer {
}
_ => {
return Err(DatabaseError::UnsupportedStatement {
statement_type: "INSERT".to_string(),
description: "INSERT with unsupported source type".to_string(),
sql: insert_stmt.to_string(),
reason: "INSERT with unsupported source type".to_string(),
});
}
},
None => {
return Err(DatabaseError::UnsupportedStatement {
statement_type: "INSERT".to_string(),
description: "INSERT statement has no source".to_string(),
reason: "INSERT statement has no source".to_string(),
sql: insert_stmt.to_string(),
});
}
}
@ -740,8 +736,8 @@ impl CrdtTransformer {
from[0].clone()
} else {
return Err(DatabaseError::UnsupportedStatement {
statement_type: "DELETE".to_string(),
description: "DELETE with multiple tables not supported".to_string(),
reason: "DELETE with multiple tables not supported".to_string(),
sql: stmt.to_string(),
});
}
}

View File

@ -156,9 +156,8 @@ pub fn select(
// Stelle sicher, dass es eine Query ist
if !matches!(statement, Statement::Query(_)) {
return Err(DatabaseError::UnsupportedStatement {
statement_type: "Non-Query".to_string(),
description: "Only SELECT statements are allowed in select function".to_string(),
return Err(DatabaseError::StatementError {
reason: "Only SELECT statements are allowed in select function".to_string(),
});
}

View File

@ -1,11 +1,10 @@
// src-tauri/src/database/error.rs
use crate::crdt::trigger::CrdtSetupError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use ts_rs::TS;
use crate::crdt::trigger::CrdtSetupError;
#[derive(Error, Debug, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(tag = "type", content = "details")]
@ -13,14 +12,21 @@ pub enum DatabaseError {
/// Der SQL-Code konnte nicht geparst werden.
#[error("Failed to parse SQL: {reason} - SQL: {sql}")]
ParseError { reason: String, sql: String },
/// Parameter-Fehler (falsche Anzahl, ungültiger Typ, etc.)
#[error("Parameter error: {reason} (expected: {expected}, provided: {provided})")]
ParamError {
reason: String,
#[error("Parameter count mismatch: SQL has {expected} placeholders but {provided} provided. SQL Statement: {sql}")]
ParameterMismatchError {
expected: usize,
provided: usize,
sql: String,
},
#[error("No table provided in SQL Statement: {sql}")]
NoTableError { sql: String },
#[error("Statement Error: {reason}")]
StatementError { reason: String },
#[error("Failed to prepare statement: {reason}")]
PrepareError { reason: String },
@ -28,7 +34,7 @@ pub enum DatabaseError {
DatabaseError { reason: String },
/// Ein Fehler ist während der Ausführung in der Datenbank aufgetreten.
#[error("Execution error on table {}: {} - SQL: {}", table.as_deref().unwrap_or("unknown"), reason, sql)]
#[error("Execution error on table {table:?}: {reason} - SQL: {sql}")]
ExecutionError {
sql: String,
reason: String,
@ -37,34 +43,36 @@ pub enum DatabaseError {
/// Ein Fehler ist beim Verwalten der Transaktion aufgetreten.
#[error("Transaction error: {reason}")]
TransactionError { reason: String },
/// Ein SQL-Statement wird vom Proxy nicht unterstützt.
#[error("Unsupported statement type '{statement_type}': {description}")]
UnsupportedStatement {
statement_type: String,
description: String,
},
#[error("Unsupported statement. '{reason}'. - SQL: {sql}")]
UnsupportedStatement { reason: String, sql: String },
/// Fehler im HLC-Service
#[error("HLC error: {reason}")]
HlcError { reason: String },
/// Fehler beim Sperren der Datenbankverbindung
#[error("Lock error: {reason}")]
LockError { reason: String },
/// Fehler bei der Datenbankverbindung
#[error("Connection error: {reason}")]
ConnectionError { reason: String },
/// Fehler bei der JSON-Serialisierung
#[error("Serialization error: {reason}")]
SerializationError { reason: String },
#[error("Permission error for extension '{extension_id}': {reason} (operation: {}, resource: {})",
operation.as_deref().unwrap_or("unknown"),
resource.as_deref().unwrap_or("unknown"))]
/// Permission-bezogener Fehler für Extensions
#[error("Permission error for extension '{extension_id}': {reason} (operation: {operation:?}, resource: {resource:?})")]
PermissionError {
extension_id: String,
operation: Option<String>,
resource: Option<String>,
reason: String,
},
#[error("Query error: {reason}")]
QueryError { reason: String },
@ -111,7 +119,43 @@ impl From<CrdtSetupError> for DatabaseError {
}
}
impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
impl DatabaseError {
/// Extract extension ID if this error is related to an extension
pub fn extension_id(&self) -> Option<&str> {
match self {
DatabaseError::PermissionError { extension_id, .. } => Some(extension_id.as_str()),
_ => None,
}
}
/// Check if this is a permission-related error
pub fn is_permission_error(&self) -> bool {
matches!(self, DatabaseError::PermissionError { .. })
}
/// Get operation if available
pub fn operation(&self) -> Option<&str> {
match self {
DatabaseError::PermissionError {
operation: Some(op),
..
} => Some(op.as_str()),
_ => None,
}
}
/// Get resource if available
pub fn resource(&self) -> Option<&str> {
match self {
DatabaseError::PermissionError {
resource: Some(res),
..
} => Some(res.as_str()),
_ => None,
}
}
}
/* impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
fn from(err: crate::extension::database::ExtensionDatabaseError) -> Self {
match err {
crate::extension::database::ExtensionDatabaseError::Permission { source } => {
@ -156,4 +200,4 @@ impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError
}
}
}
}
} */

View File

@ -13,13 +13,10 @@ use tauri::{path::BaseDirectory, AppHandle, Manager, State};
use crate::crdt::hlc::HlcService;
use crate::database::error::DatabaseError;
use crate::table_names::TABLE_CRDT_CONFIGS;
use crate::AppState;
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
pub struct AppState {
pub db: DbConnection,
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
}
#[tauri::command]
pub fn sql_select(
sql: String,
@ -166,25 +163,33 @@ pub fn create_encrypted_database(
reason: format!("Fehler beim Schließen der Quelldatenbank: {}", e),
})?;
let new_conn = core::open_and_init_db(&path, &key, false)?;
initialize_session(&app_handle, &path, &key, &state)?;
/* let new_conn = core::open_and_init_db(&path, &key, false)?;
// Aktualisieren der Datenbankverbindung im State
let mut db = state.db.0.lock().map_err(|e| DatabaseError::LockError {
reason: e.to_string(),
})?;
*db = Some(new_conn);
*db = Some(new_conn); */
Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",))
}
#[tauri::command]
pub fn open_encrypted_database(
//app_handle: AppHandle,
app_handle: AppHandle,
path: String,
key: String,
state: State<'_, AppState>,
) -> Result<String, DatabaseError> {
if !Path::new(&path).exists() {
return Err(DatabaseError::IoError {
path,
reason: "Database file not found.".to_string(),
});
}
/* let vault_path = app_handle
.path()
.resolve(format!("vaults/{}", path), BaseDirectory::AppLocalData)
@ -196,12 +201,48 @@ pub fn open_encrypted_database(
return Err(format!("File not found {}", path).into());
} */
let conn = core::open_and_init_db(&path, &key, false)
/* let conn = core::open_and_init_db(&path, &key, false)
.map_err(|e| format!("Error during open: {}", e))?;
let mut db = state.db.0.lock().map_err(|e| e.to_string())?;
*db = Some(conn);
*db = Some(conn); */
initialize_session(&app_handle, &path, &key, &state)?;
Ok(format!("success"))
}
/// Opens the DB, initializes the HLC service, and stores both in the AppState.
fn initialize_session(
app_handle: &AppHandle,
path: &str,
key: &str,
state: &State<'_, AppState>,
) -> Result<(), DatabaseError> {
// 1. Establish the raw database connection
let conn = core::open_and_init_db(path, key, false)?;
// 2. Initialize the HLC service
let hlc_service = HlcService::try_initialize(&conn, app_handle).map_err(|e| {
// We convert the HlcError into a DatabaseError
DatabaseError::ExecutionError {
sql: "HLC Initialization".to_string(),
reason: e.to_string(),
table: Some(TABLE_CRDT_CONFIGS.to_string()),
}
})?;
// 3. Store everything in the global AppState
let mut db_guard = state.db.0.lock().map_err(|e| DatabaseError::LockError {
reason: e.to_string(),
})?;
*db_guard = Some(conn);
let mut hlc_guard = state.hlc.lock().map_err(|e| DatabaseError::LockError {
reason: e.to_string(),
})?;
*hlc_guard = hlc_service;
Ok(())
}

View File

@ -1,14 +1,178 @@
/// src-tauri/src/extension/core.rs
use crate::extension::database::permissions::DbExtensionPermission;
use crate::extension::error::ExtensionError;
use crate::extension::permission_manager::ExtensionPermissions;
use mime;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
use tauri::{
http::{Request, Response},
AppHandle, Error as TauriError, Manager, Runtime, UriSchemeContext,
};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
pub entry: String,
pub icon: Option<String>,
pub permissions: ExtensionPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
/// Extension source type (production vs development)
#[derive(Debug, Clone)]
pub enum ExtensionSource {
Production {
path: PathBuf,
version: String,
},
Development {
dev_server_url: String,
manifest_path: PathBuf,
auto_reload: bool,
},
}
/// Complete extension data structure
#[derive(Debug, Clone)]
pub struct Extension {
pub id: String,
pub name: String,
pub source: ExtensionSource,
pub manifest: ExtensionManifest,
pub enabled: bool,
pub last_accessed: SystemTime,
}
/// Cached permission data for performance
#[derive(Debug, Clone)]
pub struct CachedPermission {
pub permissions: Vec<DbExtensionPermission>,
pub cached_at: SystemTime,
pub ttl: Duration,
}
/// Enhanced extension manager
#[derive(Default)]
pub struct ExtensionManager {
pub production_extensions: Mutex<HashMap<String, Extension>>,
pub dev_extensions: Mutex<HashMap<String, Extension>>,
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
}
impl ExtensionManager {
pub fn new() -> Self {
Self::default()
}
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
// Validate filesystem permissions
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
fs_perms.validate()?;
}
*/
match &extension.source {
ExtensionSource::Production { .. } => {
let mut extensions = self.production_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
}
_ => Err(ExtensionError::ValidationError {
reason: "Expected Production source".to_string(),
}),
}
}
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
// Validate filesystem permissions
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
fs_perms.validate()?;
} */
match &extension.source {
ExtensionSource::Development { .. } => {
let mut extensions = self.dev_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
}
_ => Err(ExtensionError::ValidationError {
reason: "Expected Development source".to_string(),
}),
}
}
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
// Dev extensions take priority
let dev_extensions = self.dev_extensions.lock().unwrap();
if let Some(extension) = dev_extensions.get(extension_id) {
return Some(extension.clone());
}
// Then check production
let prod_extensions = self.production_extensions.lock().unwrap();
prod_extensions.get(extension_id).cloned()
}
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
{
let mut dev_extensions = self.dev_extensions.lock().unwrap();
if dev_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
{
let mut prod_extensions = self.production_extensions.lock().unwrap();
if prod_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
Err(ExtensionError::NotFound {
id: extension_id.to_string(),
})
}
}
// For backward compatibility
#[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
}
impl ExtensionState {
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
let mut extensions = self.extensions.lock().unwrap();
extensions.insert(path, manifest);
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
let extensions = self.extensions.lock().unwrap();
extensions.values().find(|p| p.name == addon_id).cloned()
}
}
#[derive(Deserialize, Debug)]
struct ExtensionInfo {
id: String,

View File

@ -1,14 +1,14 @@
// src-tauri/src/extension/database/mod.rs
pub mod permissions;
use crate::crdt::hlc::HlcService;
use crate::crdt::transformer::CrdtTransformer;
use crate::crdt::trigger;
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
use crate::database::error::DatabaseError;
use crate::database::AppState;
use permissions::{check_read_permission, check_write_permission, PermissionError};
use crate::extension::error::ExtensionError;
use crate::AppState;
use permissions::{check_read_permission, check_write_permission};
use rusqlite::params_from_iter;
use rusqlite::types::Value as SqlValue;
use rusqlite::Transaction;
@ -17,36 +17,6 @@ use serde_json::Value as JsonValue;
use sqlparser::ast::{Statement, TableFactor, TableObject};
use std::collections::HashSet;
use tauri::State;
use thiserror::Error;
/// Combined error type für Extension-Database operations
#[derive(Error, Debug)]
pub enum ExtensionDatabaseError {
#[error("Permission denied: {source}")]
Permission {
#[from]
source: PermissionError,
},
#[error("Database error: {source}")]
Database {
#[from]
source: DatabaseError,
},
#[error("Parameter validation failed: {reason}")]
ParameterValidation { reason: String },
#[error("Statement execution failed: {reason}")]
StatementExecution { reason: String },
}
// Für Tauri Command Serialization
impl serde::Serialize for ExtensionDatabaseError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
/// Führt Statements mit korrekter Parameter-Bindung aus
pub struct StatementExecutor<'a> {
@ -67,30 +37,27 @@ impl<'a> StatementExecutor<'a> {
&self,
statement: &Statement,
params: &[SqlValue],
) -> Result<(), ExtensionDatabaseError> {
) -> Result<(), DatabaseError> {
let sql = statement.to_string();
let expected_params = count_sql_placeholders(&sql);
if expected_params != params.len() {
return Err(ExtensionDatabaseError::ParameterValidation {
reason: format!(
"Parameter count mismatch for statement: {} (expected: {}, provided: {})",
truncate_sql(&sql, 100),
expected_params,
params.len()
),
return Err(DatabaseError::ParameterMismatchError {
expected: expected_params,
provided: params.len(),
sql,
});
}
self.transaction
.execute(&sql, params_from_iter(params.iter()))
.map_err(|e| ExtensionDatabaseError::StatementExecution {
reason: format!(
"Failed to execute statement on table {}: {}",
.map_err(|e| DatabaseError::ExecutionError {
sql,
table: Some(
self.extract_table_name_from_statement(statement)
.unwrap_or_else(|| "unknown".to_string()),
e
),
reason: e.to_string(),
})?;
Ok(())
@ -147,7 +114,7 @@ pub async fn extension_sql_execute(
extension_id: String,
state: State<'_, AppState>,
hlc_service: State<'_, HlcService>,
) -> Result<Vec<String>, ExtensionDatabaseError> {
) -> Result<Vec<String>, ExtensionError> {
// Permission check
check_write_permission(&state.db, &extension_id, sql).await?;
@ -208,7 +175,7 @@ pub async fn extension_sql_execute(
Ok(modified_schema_tables.into_iter().collect())
})
.map_err(ExtensionDatabaseError::from)
.map_err(ExtensionError::from)
}
#[tauri::command]
@ -217,7 +184,7 @@ pub async fn extension_sql_select(
params: Vec<JsonValue>,
extension_id: String,
state: State<'_, AppState>,
) -> Result<Vec<JsonValue>, ExtensionDatabaseError> {
) -> Result<Vec<JsonValue>, ExtensionError> {
// Permission check
check_read_permission(&state.db, &extension_id, sql).await?;
@ -234,8 +201,13 @@ pub async fn extension_sql_select(
// Validate that all statements are queries
for stmt in &ast_vec {
if !matches!(stmt, Statement::Query(_)) {
return Err(ExtensionDatabaseError::StatementExecution {
reason: "Only SELECT statements are allowed in extension_sql_select".to_string(),
return Err(ExtensionError::Database {
source: DatabaseError::ExecutionError {
sql: sql.to_string(),
reason: "Only SELECT statements are allowed in extension_sql_select"
.to_string(),
table: None,
},
});
}
}
@ -285,7 +257,7 @@ pub async fn extension_sql_select(
Ok(results)
})
.map_err(ExtensionDatabaseError::from)
.map_err(ExtensionError::from)
}
/// Konvertiert eine SQLite-Zeile zu JSON
@ -309,16 +281,14 @@ fn row_to_json_value(
}
/// Validiert Parameter gegen SQL-Platzhalter
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), ExtensionDatabaseError> {
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
let total_placeholders = count_sql_placeholders(sql);
if total_placeholders != params.len() {
return Err(ExtensionDatabaseError::ParameterValidation {
reason: format!(
"Parameter count mismatch: SQL has {} placeholders but {} parameters provided",
total_placeholders,
params.len()
),
return Err(DatabaseError::ParameterMismatchError {
expected: total_placeholders,
provided: params.len(),
sql: sql.to_string(),
});
}

View File

@ -5,52 +5,18 @@ use crate::database::core::{
};
use crate::database::error::DatabaseError;
use crate::database::DbConnection;
use crate::models::DbExtensionPermission;
use crate::extension::error::ExtensionError;
use serde::{Deserialize, Serialize};
use sqlparser::ast::{Statement, TableFactor, TableObject};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize)]
pub enum PermissionError {
#[error("Extension '{extension_id}' has no {operation} permission for {resource}: {reason}")]
AccessDenied {
extension_id: String,
operation: String,
resource: String,
reason: String,
},
#[error("Database error while checking permissions: {source}")]
Database {
#[from]
source: DatabaseError,
},
#[error("SQL parsing error: {reason}")]
SqlParse { reason: String },
#[error("Invalid SQL statement: {reason}")]
InvalidStatement { reason: String },
#[error("No SQL statement found")]
NoStatement,
#[error("Unsupported statement type for permission check")]
UnsupportedStatement,
#[error("No table specified in {statement_type} statement")]
NoTableSpecified { statement_type: String },
}
// Hilfsfunktion für bessere Lesbarkeit
impl PermissionError {
pub fn access_denied(
extension_id: &str,
operation: &str,
resource: &str,
reason: &str,
) -> Self {
Self::AccessDenied {
extension_id: extension_id.to_string(),
operation: operation.to_string(),
resource: resource.to_string(),
reason: reason.to_string(),
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DbExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource: String,
pub operation: String,
pub path: String,
}
/// Prüft Leseberechtigungen für eine Extension
@ -58,9 +24,10 @@ pub async fn check_read_permission(
connection: &DbConnection,
extension_id: &str,
sql: &str,
) -> Result<(), PermissionError> {
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
) -> Result<(), ExtensionError> {
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})?;
match statement {
@ -68,9 +35,11 @@ pub async fn check_read_permission(
let tables = extract_table_names_from_sql(&query.to_string())?;
check_table_permissions(connection, extension_id, &tables, "read").await
}
_ => Err(PermissionError::InvalidStatement {
_ => Err(DatabaseError::UnsupportedStatement {
reason: "Only SELECT statements are allowed for read operations".to_string(),
}),
sql: sql.to_string(),
}
.into()),
}
}
@ -79,9 +48,10 @@ pub async fn check_write_permission(
connection: &DbConnection,
extension_id: &str,
sql: &str,
) -> Result<(), PermissionError> {
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
) -> Result<(), ExtensionError> {
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})?;
match statement {
@ -111,38 +81,44 @@ pub async fn check_write_permission(
let table_names: Vec<String> = names.iter().map(|name| name.to_string()).collect();
check_table_permissions(connection, extension_id, &table_names, "drop").await
}
_ => Err(PermissionError::UnsupportedStatement),
_ => Err(DatabaseError::UnsupportedStatement {
reason: "SQL Statement is not allowed".to_string(),
sql: sql.to_string(),
}
.into()),
}
}
/// Extrahiert Tabellenname aus INSERT-Statement
fn extract_table_name_from_insert(
insert: &sqlparser::ast::Insert,
) -> Result<String, PermissionError> {
) -> Result<String, ExtensionError> {
match &insert.table {
TableObject::TableName(name) => Ok(name.to_string()),
_ => Err(PermissionError::NoTableSpecified {
statement_type: "INSERT".to_string(),
}),
_ => Err(DatabaseError::NoTableError {
sql: insert.to_string(),
}
.into()),
}
}
/// Extrahiert Tabellenname aus TableFactor
fn extract_table_name_from_table_factor(
table_factor: &TableFactor,
) -> Result<String, PermissionError> {
) -> Result<String, ExtensionError> {
match table_factor {
TableFactor::Table { name, .. } => Ok(name.to_string()),
_ => Err(PermissionError::InvalidStatement {
_ => Err(DatabaseError::StatementError {
reason: "Complex table references not supported".to_string(),
}),
}
.into()),
}
}
/// Extrahiert Tabellenname aus DELETE-Statement
fn extract_table_name_from_delete(
delete: &sqlparser::ast::Delete,
) -> Result<String, PermissionError> {
) -> Result<String, ExtensionError> {
use sqlparser::ast::FromTable;
let table_name = match &delete.from {
@ -152,9 +128,10 @@ fn extract_table_name_from_delete(
} else if !delete.tables.is_empty() {
delete.tables[0].to_string()
} else {
return Err(PermissionError::NoTableSpecified {
statement_type: "DELETE".to_string(),
});
return Err(DatabaseError::NoTableError {
sql: delete.to_string(),
}
.into());
}
}
};
@ -168,7 +145,7 @@ async fn check_single_table_permission(
extension_id: &str,
table_name: &str,
operation: &str,
) -> Result<(), PermissionError> {
) -> Result<(), ExtensionError> {
check_table_permissions(
connection,
extension_id,
@ -184,7 +161,7 @@ async fn check_table_permissions(
extension_id: &str,
table_names: &[String],
operation: &str,
) -> Result<(), PermissionError> {
) -> Result<(), ExtensionError> {
let permissions =
get_extension_permissions(connection, extension_id, "database", operation).await?;
@ -194,11 +171,10 @@ async fn check_table_permissions(
.any(|perm| perm.path.contains(table_name));
if !has_permission {
return Err(PermissionError::access_denied(
return Err(ExtensionError::permission_denied(
extension_id,
operation,
&format!("table '{}'", table_name),
"Table not in permitted resources",
));
}
}
@ -207,7 +183,7 @@ async fn check_table_permissions(
}
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
async fn get_extension_permissions(
pub async fn get_extension_permissions(
connection: &DbConnection,
extension_id: &str,
resource: &str,
@ -240,10 +216,7 @@ async fn get_extension_permissions(
let mut permissions = Vec::new();
for row_result in rows {
let permission = row_result.map_err(|e| DatabaseError::PermissionError {
extension_id: extension_id.to_string(),
operation: Some(operation.to_string()),
resource: Some(resource.to_string()),
let permission = row_result.map_err(|e| DatabaseError::DatabaseError {
reason: e.to_string(),
})?;
permissions.push(permission);
@ -255,6 +228,8 @@ async fn get_extension_permissions(
#[cfg(test)]
mod tests {
use crate::extension::error::ExtensionError;
use super::*;
#[test]
@ -269,7 +244,7 @@ mod tests {
fn test_parse_invalid_sql() {
let sql = "INVALID SQL";
let result = parse_single_statement(sql);
// parse_single_statement gibt DatabaseError zurück, nicht PermissionError
// parse_single_statement gibt DatabaseError zurück, nicht DatabaseError
assert!(result.is_err());
// Wenn du spezifischer sein möchtest, kannst du den DatabaseError-Typ prüfen:
match result {
@ -284,11 +259,11 @@ mod tests {
}
}
#[test]
/* #[test]
fn test_permission_error_access_denied() {
let error = PermissionError::access_denied("ext1", "read", "table1", "not allowed");
let error = ExtensionError::access_denied("ext1", "read", "table1", "not allowed");
match error {
PermissionError::AccessDenied {
ExtensionError::AccessDenied {
extension_id,
operation,
resource,
@ -301,5 +276,5 @@ mod tests {
}
_ => panic!("Expected AccessDenied error"),
}
}
} */
}

View File

@ -0,0 +1,214 @@
/// src-tauri/src/extension/error.rs
use thiserror::Error;
use crate::database::error::DatabaseError;
/// Comprehensive error type for extension operations
#[derive(Error, Debug)]
pub enum ExtensionError {
#[error("Security violation: {reason}")]
SecurityViolation { reason: String },
#[error("Extension not found: {id}")]
NotFound { id: String },
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
PermissionDenied {
extension_id: String,
operation: String,
resource: String,
},
#[error("Database operation failed: {source}")]
Database {
#[from]
source: DatabaseError,
},
#[error("Filesystem operation failed: {source}")]
Filesystem {
#[from]
source: std::io::Error,
// oder: source: FilesystemError,
},
#[error("HTTP request failed: {reason}")]
Http {
reason: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Shell command failed: {reason}")]
Shell {
reason: String,
exit_code: Option<i32>,
},
/* #[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
}, */
#[error("Manifest error: {reason}")]
ManifestError { reason: String },
#[error("Validation error: {reason}")]
ValidationError { reason: String },
#[error("Dev server error: {reason}")]
DevServerError { reason: String },
#[error("Serialization error: {reason}")]
SerializationError { reason: String },
#[error("Configuration error: {reason}")]
ConfigError { reason: String },
}
impl ExtensionError {
/// Convenience constructor for permission denied errors
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
Self::PermissionDenied {
extension_id: extension_id.to_string(),
operation: operation.to_string(),
resource: resource.to_string(),
}
}
/// Convenience constructor for HTTP errors
pub fn http_error(reason: &str) -> Self {
Self::Http {
reason: reason.to_string(),
source: None,
}
}
/// Convenience constructor for HTTP errors with source
pub fn http_error_with_source(
reason: &str,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::Http {
reason: reason.to_string(),
source: Some(source),
}
}
/// Convenience constructor for shell errors
pub fn shell_error(reason: &str, exit_code: Option<i32>) -> Self {
Self::Shell {
reason: reason.to_string(),
exit_code,
}
}
/// Check if this error is related to permissions
pub fn is_permission_error(&self) -> bool {
matches!(
self,
ExtensionError::PermissionDenied { .. } | ExtensionError::SecurityViolation { .. }
)
}
/// Extract extension ID if available
pub fn extension_id(&self) -> Option<&str> {
match self {
ExtensionError::PermissionDenied { extension_id, .. } => Some(extension_id),
ExtensionError::Database { source } => source.extension_id(),
_ => None,
}
}
}
impl serde::Serialize for ExtensionError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
// Error type as discriminator
let error_type = match self {
ExtensionError::SecurityViolation { .. } => "SecurityViolation",
ExtensionError::NotFound { .. } => "NotFound",
ExtensionError::PermissionDenied { .. } => "PermissionDenied",
ExtensionError::Database { .. } => "Database",
ExtensionError::Filesystem { .. } => "Filesystem",
ExtensionError::Http { .. } => "Http",
ExtensionError::Shell { .. } => "Shell",
//ExtensionError::Io { .. } => "Io",
ExtensionError::ManifestError { .. } => "ManifestError",
ExtensionError::ValidationError { .. } => "ValidationError",
ExtensionError::DevServerError { .. } => "DevServerError",
ExtensionError::SerializationError { .. } => "SerializationError",
ExtensionError::ConfigError { .. } => "ConfigError",
};
state.serialize_field("type", error_type)?;
state.serialize_field("message", &self.to_string())?;
// Add extension_id if available
if let Some(ext_id) = self.extension_id() {
state.serialize_field("extension_id", ext_id)?;
} else {
state.serialize_field("extension_id", &Option::<String>::None)?;
}
state.end()
}
}
// For Tauri command serialization
impl From<serde_json::Error> for ExtensionError {
fn from(err: serde_json::Error) -> Self {
ExtensionError::SerializationError {
reason: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::error::DatabaseError;
/* #[test]
fn test_database_error_conversion() {
let db_error = DatabaseError::access_denied("ext1", "read", "users", "no permission");
let ext_error: ExtensionError = db_error.into();
assert!(ext_error.is_permission_error());
assert_eq!(ext_error.extension_id(), Some("ext1"));
} */
#[test]
fn test_permission_denied_constructor() {
let error = ExtensionError::permission_denied("ext1", "write", "config.json");
match error {
ExtensionError::PermissionDenied {
extension_id,
operation,
resource,
} => {
assert_eq!(extension_id, "ext1");
assert_eq!(operation, "write");
assert_eq!(resource, "config.json");
}
_ => panic!("Expected PermissionDenied error"),
}
}
#[test]
fn test_serialization() {
let error = ExtensionError::permission_denied("ext1", "read", "database");
let serialized = serde_json::to_string(&error).unwrap();
// Basic check that it serializes properly
assert!(serialized.contains("PermissionDenied"));
assert!(serialized.contains("ext1"));
}
}

View File

@ -0,0 +1,186 @@
use serde::{Deserialize, Serialize};
use crate::extension::error::ExtensionError;
/// Simple filesystem permissions using path patterns with environment-style variables
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPermissions {
/// Read access to files and directories
/// Examples: ["$DOCUMENT/**", "$PICTURE/*.jpg", "$APPDATA/my-extension/*"]
pub read: Option<Vec<String>>,
/// Write access to files and directories (includes create/delete)
/// Examples: ["$APPDATA/my-extension/**", "$DOWNLOAD/*.pdf"]
pub write: Option<Vec<String>>,
}
impl FilesystemPermissions {
/// Helper to create common permission patterns
pub fn new() -> Self {
Self {
read: None,
write: None,
}
}
/// Add read permission for a path pattern
pub fn add_read(&mut self, pattern: &str) {
match &mut self.read {
Some(patterns) => patterns.push(pattern.to_string()),
None => self.read = Some(vec![pattern.to_string()]),
}
}
/// Add write permission for a path pattern
pub fn add_write(&mut self, pattern: &str) {
match &mut self.write {
Some(patterns) => patterns.push(pattern.to_string()),
None => self.write = Some(vec![pattern.to_string()]),
}
}
/// Helper: Add extension's own data directory permissions
pub fn extension_data(extension_id: &str) -> Self {
Self {
read: Some(vec![format!("$APPDATA/extensions/{}/**", extension_id)]),
write: Some(vec![format!("$APPDATA/extensions/{}/**", extension_id)]),
}
}
/// Helper: Add document access permissions
pub fn documents_read_only() -> Self {
Self {
read: Some(vec!["$DOCUMENT/**".to_string()]),
write: None,
}
}
/// Helper: Add picture access permissions
pub fn pictures_read_only() -> Self {
Self {
read: Some(vec!["$PICTURE/**".to_string()]),
write: None,
}
}
/// Validate all path patterns
pub fn validate(&self) -> Result<(), ExtensionError> {
if let Some(read_patterns) = &self.read {
for pattern in read_patterns {
validate_path_pattern(pattern)?;
}
}
if let Some(write_patterns) = &self.write {
for pattern in write_patterns {
validate_path_pattern(pattern)?;
}
}
Ok(())
}
}
/// Validates a filesystem path pattern
fn validate_path_pattern(pattern: &str) -> Result<(), ExtensionError> {
if pattern.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Path pattern cannot be empty".to_string(),
});
}
// Check if pattern starts with valid base directory variable
let valid_bases = [
"$APPDATA",
"$APPCACHE",
"$APPCONFIG",
"$APPLOCALDATA",
"$APPLOG",
"$AUDIO",
"$CACHE",
"$CONFIG",
"$DATA",
"$LOCALDATA",
"$DESKTOP",
"$DOCUMENT",
"$DOWNLOAD",
"$EXECUTABLE",
"$FONT",
"$HOME",
"$PICTURE",
"$PUBLIC",
"$RUNTIME",
"$TEMPLATE",
"$VIDEO",
"$RESOURCE",
"$TEMP",
];
let starts_with_valid_base = valid_bases.iter().any(|&base| {
pattern.starts_with(base)
&& (pattern.len() == base.len() || pattern.chars().nth(base.len()) == Some('/'))
});
if !starts_with_valid_base {
return Err(ExtensionError::ValidationError {
reason: format!(
"Path pattern '{}' must start with a valid base directory: {}",
pattern,
valid_bases.join(", ")
),
});
}
// Check for path traversal attempts
if pattern.contains("../") || pattern.contains("..\\") {
return Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal detected in pattern: {}", pattern),
});
}
Ok(())
}
/// Resolves a path pattern to actual filesystem paths using Tauri's BaseDirectory
pub fn resolve_path_pattern(
pattern: &str,
app_handle: &tauri::AppHandle,
) -> Result<(String, String), ExtensionError> {
let (base_var, relative_path) = if let Some(slash_pos) = pattern.find('/') {
(&pattern[..slash_pos], &pattern[slash_pos + 1..])
} else {
(pattern, "")
};
let base_directory = match base_var {
"$APPDATA" => "AppData",
"$APPCACHE" => "AppCache",
"$APPCONFIG" => "AppConfig",
"$APPLOCALDATA" => "AppLocalData",
"$APPLOG" => "AppLog",
"$AUDIO" => "Audio",
"$CACHE" => "Cache",
"$CONFIG" => "Config",
"$DATA" => "Data",
"$LOCALDATA" => "LocalData",
"$DESKTOP" => "Desktop",
"$DOCUMENT" => "Document",
"$DOWNLOAD" => "Download",
"$EXECUTABLE" => "Executable",
"$FONT" => "Font",
"$HOME" => "Home",
"$PICTURE" => "Picture",
"$PUBLIC" => "Public",
"$RUNTIME" => "Runtime",
"$TEMPLATE" => "Template",
"$VIDEO" => "Video",
"$RESOURCE" => "Resource",
"$TEMP" => "Temp",
_ => {
return Err(ExtensionError::ValidationError {
reason: format!("Unknown base directory variable: {}", base_var),
});
}
};
Ok((base_directory.to_string(), relative_path.to_string()))
}

View File

@ -0,0 +1,2 @@
pub mod core;
pub mod permissions;

View File

@ -0,0 +1,101 @@
use serde::{Deserialize, Serialize};
use crate::extension::error::ExtensionError;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPermissions {
/// Read access to files and directories
pub read: Option<Vec<FilesystemPath>>,
/// Write access to files and directories (includes create/delete)
pub write: Option<Vec<FilesystemPath>>,
}
/// Cross-platform filesystem path specification
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPath {
/// The type of path (determines base directory)
pub path_type: FilesystemPathType,
/// Relative path from the base directory
pub relative_path: String,
/// Whether subdirectories are included (recursive)
pub recursive: bool,
}
/// Platform-agnostic path types that map to appropriate directories on each OS
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum FilesystemPathType {
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
AppData,
/// App's cache directory
AppCache,
/// App's configuration directory
AppConfig,
/// User's documents directory
Documents,
/// User's pictures directory
Pictures,
/// User's downloads directory
Downloads,
/// Temporary directory
Temp,
/// Extension's own private directory (always allowed)
ExtensionData,
/// Shared data between extensions (requires special permission)
SharedData,
}
impl FilesystemPath {
/// Creates a new filesystem path specification
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
Self {
path_type,
relative_path: relative_path.to_string(),
recursive,
}
}
/// Resolves the path to an actual system path
/// This would be implemented in your Tauri backend
pub fn resolve_system_path(
&self,
app_handle: &tauri::AppHandle,
) -> Result<String, ExtensionError> {
/* let base_dir = match self.path_type {
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
FilesystemPathType::Documents => app_handle.path().document_dir(),
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
FilesystemPathType::Downloads => app_handle.path().download_dir(),
FilesystemPathType::Temp => app_handle.path().temp_dir(),
FilesystemPathType::ExtensionData => app_handle
.path()
.app_data_dir()
.map(|p| p.join("extensions")),
FilesystemPathType::SharedData => {
app_handle.path().app_data_dir().map(|p| p.join("shared"))
}
}
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to resolve base directory: {}", e),
})?;
let final_path = base_dir.join(&self.relative_path);
// Security check - ensure the resolved path is still within the base directory
if let (Ok(canonical_final), Ok(canonical_base)) =
(final_path.canonicalize(), base_dir.canonicalize())
{
if !canonical_final.starts_with(&canonical_base) {
return Err(ExtensionError::SecurityViolation {
reason: format!(
"Path traversal detected: {} escapes base directory",
self.relative_path
),
});
}
} */
Ok("".to_string())
}
}

View File

@ -1,2 +1,5 @@
pub mod core;
pub mod database;
pub mod error;
pub mod filesystem;
pub mod permission_manager;

View File

@ -0,0 +1,297 @@
/// src-tauri/src/extension/permission_manager.rs
use crate::extension::error::ExtensionError;
use crate::database::DbConnection;
use crate::extension::database::permissions::DbExtensionPermission;
use serde::{Deserialize, Serialize};
use tauri::Url;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ExtensionPermissions {
pub database: Vec<DbExtensionPermission>,
pub filesystem: Vec<FilesystemPermission>,
pub http: Vec<HttpPermission>,
pub shell: Vec<ShellPermission>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FilesystemPermission {
pub extension_id: String,
pub operation: String, // read, write, create, delete
pub path: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HttpPermission {
pub extension_id: String,
pub operation: String, // get, post, put, delete
pub domain: String,
pub path_pattern: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ShellPermission {
pub extension_id: String,
pub command: String,
pub arguments: Vec<String>,
}
/// Zentraler Permission Manager
pub struct PermissionManager;
impl PermissionManager {
/// Prüft Datenbankberechtigungen
pub async fn check_database_permission(
connection: &DbConnection,
extension_id: &str,
operation: &str,
table_name: &str,
) -> Result<(), ExtensionError> {
let permissions = Self::get_database_permissions(connection, extension_id, operation).await?;
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(table_name));
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
operation,
&format!("database table '{}'", table_name),
));
}
Ok(())
}
/// Prüft Dateisystem-Berechtigungen
pub async fn check_filesystem_permission(
connection: &DbConnection,
extension_id: &str,
operation: &str,
file_path: &Path,
) -> Result<(), ExtensionError> {
let permissions = Self::get_filesystem_permissions(connection, extension_id, operation).await?;
let file_path_str = file_path.to_string_lossy();
let has_permission = permissions.iter().any(|perm| {
// Prüfe, ob der Pfad mit einem erlaubten Pfad beginnt oder übereinstimmt
file_path_str.starts_with(&perm.path) ||
// Oder ob es ein Wildcard-Match gibt
Self::matches_path_pattern(&perm.path, &file_path_str)
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
operation,
&format!("filesystem path '{}'", file_path_str),
));
}
Ok(())
}
/// Prüft HTTP-Berechtigungen
pub async fn check_http_permission(
connection: &DbConnection,
extension_id: &str,
method: &str,
url: &str,
) -> Result<(), ExtensionError> {
let permissions = Self::get_http_permissions(connection, extension_id, method).await?;
let url_parsed = Url::parse(url).map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Invalid URL: {}", e),
}
})?;
let domain = url_parsed.host_str().unwrap_or("");
let path = url_parsed.path();
let has_permission = permissions.iter().any(|perm| {
// Prüfe Domain
let domain_matches = perm.domain == "*" ||
perm.domain == domain ||
domain.ends_with(&format!(".{}", perm.domain));
// Prüfe Pfad (falls spezifiziert)
let path_matches = perm.path_pattern.as_ref()
.map(|pattern| Self::matches_path_pattern(pattern, path))
.unwrap_or(true);
domain_matches && path_matches
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
method,
&format!("HTTP request to '{}'", url),
));
}
Ok(())
}
/// Prüft Shell-Berechtigungen
pub async fn check_shell_permission(
connection: &DbConnection,
extension_id: &str,
command: &str,
args: &[String],
) -> Result<(), ExtensionError> {
let permissions = Self::get_shell_permissions(connection, extension_id).await?;
let has_permission = permissions.iter().any(|perm| {
// Prüfe Command
if perm.command != command && perm.command != "*" {
return false;
}
// Prüfe Arguments (falls spezifiziert)
if !perm.arguments.is_empty() {
// Alle erforderlichen Args müssen vorhanden sein
perm.arguments.iter().all(|required_arg| {
args.iter().any(|actual_arg| {
required_arg == actual_arg || required_arg == "*"
})
})
} else {
true
}
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
"execute",
&format!("shell command '{}' with args {:?}", command, args),
));
}
Ok(())
}
// Private Helper-Methoden
async fn get_database_permissions(
connection: &DbConnection,
extension_id: &str,
operation: &str,
) -> Result<Vec<DbExtensionPermission>, ExtensionError> {
// Verwende die bestehende Funktion aus dem permissions.rs
crate::extension::database::permissions::get_extension_permissions(
connection,
extension_id,
"database",
operation
).await.map_err(ExtensionError::from)
}
async fn get_filesystem_permissions(
connection: &DbConnection,
extension_id: &str,
operation: &str,
) -> Result<Vec<FilesystemPermission>, ExtensionError> {
// Implementierung für Filesystem-Permissions
// Ähnlich wie get_database_permissions, aber für filesystem Tabelle
todo!("Implementiere Filesystem-Permission-Loading")
}
async fn get_http_permissions(
connection: &DbConnection,
extension_id: &str,
method: &str,
) -> Result<Vec<HttpPermission>, ExtensionError> {
// Implementierung für HTTP-Permissions
todo!("Implementiere HTTP-Permission-Loading")
}
async fn get_shell_permissions(
connection: &DbConnection,
extension_id: &str,
) -> Result<Vec<ShellPermission>, ExtensionError> {
// Implementierung für Shell-Permissions
todo!("Implementiere Shell-Permission-Loading")
}
fn matches_path_pattern(pattern: &str, path: &str) -> bool {
// Einfache Wildcard-Implementierung
if pattern.ends_with('*') {
let prefix = &pattern[..pattern.len() - 1];
path.starts_with(prefix)
} else if pattern.starts_with('*') {
let suffix = &pattern[1..];
path.ends_with(suffix)
} else {
pattern == path
}
}
}
// Convenience-Funktionen für die verschiedenen Subsysteme
impl PermissionManager {
/// Convenience für Datei lesen
pub async fn can_read_file(
connection: &DbConnection,
extension_id: &str,
file_path: &Path,
) -> Result<(), ExtensionError> {
Self::check_filesystem_permission(connection, extension_id, "read", file_path).await
}
/// Convenience für Datei schreiben
pub async fn can_write_file(
connection: &DbConnection,
extension_id: &str,
file_path: &Path,
) -> Result<(), ExtensionError> {
Self::check_filesystem_permission(connection, extension_id, "write", file_path).await
}
/// Convenience für HTTP GET
pub async fn can_http_get(
connection: &DbConnection,
extension_id: &str,
url: &str,
) -> Result<(), ExtensionError> {
Self::check_http_permission(connection, extension_id, "GET", url).await
}
/// Convenience für HTTP POST
pub async fn can_http_post(
connection: &DbConnection,
extension_id: &str,
url: &str,
) -> Result<(), ExtensionError> {
Self::check_http_permission(connection, extension_id, "POST", url).await
}
/// Convenience für Shell-Befehl
pub async fn can_execute_command(
connection: &DbConnection,
extension_id: &str,
command: &str,
args: &[String],
) -> Result<(), ExtensionError> {
Self::check_shell_permission(connection, extension_id, command, args).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_pattern_matching() {
assert!(PermissionManager::matches_path_pattern("/home/user/*", "/home/user/documents/file.txt"));
assert!(PermissionManager::matches_path_pattern("*.txt", "/path/to/file.txt"));
assert!(PermissionManager::matches_path_pattern("/exact/path", "/exact/path"));
assert!(!PermissionManager::matches_path_pattern("/home/user/*", "/etc/passwd"));
assert!(!PermissionManager::matches_path_pattern("*.txt", "/path/to/file.pdf"));
}
}

View File

@ -3,27 +3,34 @@
mod crdt;
mod database;
mod extension;
mod models;
//mod models;
pub mod table_names {
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
}
use models::ExtensionState;
use std::sync::{Arc, Mutex};
use crate::{
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionState};
/* use crate::{
crdt::hlc::HlcService,
database::{AppState, DbConnection},
};
extension::core::ExtensionState,
}; */
pub struct AppState {
pub db: DbConnection,
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let protocol_name = "haex-extension";
tauri::Builder::default()
/* .register_uri_scheme_protocol(protocol_name, move |context, request| {
.register_uri_scheme_protocol(protocol_name, move |context, request| {
match extension::core::extension_protocol_handler(&context, &request) {
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
Err(e) => {
@ -52,7 +59,7 @@ pub fn run() {
})
}
}
}) */
})
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
.manage(crdt::hlc::HlcService::new()) */
.manage(AppState {

View File

@ -1,28 +1,45 @@
// models.rs
// src-tauri/src/models.rs
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use thiserror::Error;
#[derive(Serialize, Deserialize, Clone)]
/* #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
pub entry: String,
pub icon: Option<String>,
pub permissions: ExtensionPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionPermissions {
pub database: Option<DatabasePermissions>,
pub http: Option<Vec<String>>,
pub filesystem: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct DatabasePermissions {
pub read: Option<Vec<String>>,
pub write: Option<Vec<String>>,
pub create: Option<Vec<String>>,
}
/// Enum to represent the source of an extension
#[derive(Debug, Clone)]
pub enum ExtensionSource {
/// Production extension installed in app data
Production { path: PathBuf, version: String },
/// Development mode extension with live reloading
Development {
dev_server_url: String,
manifest_path: PathBuf,
auto_reload: bool,
},
} */
/*
#[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
@ -48,3 +65,43 @@ pub struct DbExtensionPermission {
pub operation: String,
pub path: String,
}
/// Comprehensive error type for all extension-related operations
#[derive(Error, Debug)]
pub enum ExtensionError {
/// Security violation detected
#[error("Security violation: {reason}")]
SecurityViolation { reason: String },
/// Extension not found
#[error("Extension not found: {id}")]
NotFound { id: String },
/// Permission denied
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
PermissionDenied {
extension_id: String,
operation: String,
resource: String,
},
/// IO error during extension operations
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
},
/// Error during extension manifest parsing
#[error("Manifest error: {reason}")]
ManifestError { reason: String },
/// Input validation error
#[error("Validation error: {reason}")]
ValidationError { reason: String },
/// Development server error
#[error("Dev server error: {reason}")]
DevServerError { reason: String },
}
*/

View File

@ -0,0 +1,34 @@
// models.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_path_pattern() {
// Valid patterns
assert!(validate_path_pattern("$PICTURE/**").is_ok());
assert!(validate_path_pattern("$DOCUMENT/myfiles/*").is_ok());
assert!(validate_path_pattern("$APPDATA/extensions/my-ext/**").is_ok());
// Invalid patterns
assert!(validate_path_pattern("").is_err());
assert!(validate_path_pattern("$INVALID/test").is_err());
assert!(validate_path_pattern("$PICTURE/../secret").is_err());
assert!(validate_path_pattern("relative/path").is_err());
}
#[test]
fn test_filesystem_permissions() {
let mut perms = FilesystemPermissions::new();
perms.add_read("$PICTURE/**");
perms.add_write("$APPDATA/my-ext/**");
assert!(perms.validate().is_ok());
assert_eq!(perms.read.as_ref().unwrap().len(), 1);
assert_eq!(perms.write.as_ref().unwrap().len(), 1);
}
}

441
src-tauri/src/models_new.rs Normal file
View File

@ -0,0 +1,441 @@
// models.rs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, Arc};
use std::time::{Duration, SystemTime};
use thiserror::Error;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionManifest {
pub id: String,
pub name: String,
pub version: String,
pub author: Option<String>,
pub entry: String,
pub icon: Option<String>,
pub permissions: ExtensionPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionPermissions {
pub database: Option<DatabasePermissions>,
pub http: Option<Vec<String>>,
pub filesystem: Option<FilesystemPermissions>,
pub shell: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DatabasePermissions {
pub read: Option<Vec<String>>,
pub write: Option<Vec<String>>,
pub create: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPermissions {
/// Read access to files and directories
pub read: Option<Vec<FilesystemPath>>,
/// Write access to files and directories (includes create/delete)
pub write: Option<Vec<FilesystemPath>>,
}
/// Cross-platform filesystem path specification
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FilesystemPath {
/// The type of path (determines base directory)
pub path_type: FilesystemPathType,
/// Relative path from the base directory
pub relative_path: String,
/// Whether subdirectories are included (recursive)
pub recursive: bool,
}
/// Platform-agnostic path types that map to appropriate directories on each OS
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum FilesystemPathType {
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
AppData,
/// App's cache directory
AppCache,
/// App's configuration directory
AppConfig,
/// User's documents directory
Documents,
/// User's pictures directory
Pictures,
/// User's downloads directory
Downloads,
/// Temporary directory
Temp,
/// Extension's own private directory (always allowed)
ExtensionData,
/// Shared data between extensions (requires special permission)
SharedData,
}
impl FilesystemPath {
/// Creates a new filesystem path specification
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
Self {
path_type,
relative_path: relative_path.to_string(),
recursive,
}
}
/// Resolves the path to an actual system path
/// This would be implemented in your Tauri backend
pub fn resolve_system_path(&self, app_handle: &tauri::AppHandle) -> Result<std::path::PathBuf, ExtensionError> {
let base_dir = match self.path_type {
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
FilesystemPathType::Documents => app_handle.path().document_dir(),
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
FilesystemPathType::Downloads => app_handle.path().download_dir(),
FilesystemPathType::Temp => app_handle.path().temp_dir(),
FilesystemPathType::ExtensionData => {
app_handle.path().app_data_dir().map(|p| p.join("extensions"))
},
FilesystemPathType::SharedData => {
app_handle.path().app_data_dir().map(|p| p.join("shared"))
},
}.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to resolve base directory: {}", e),
})?;
let final_path = base_dir.join(&self.relative_path);
// Security check - ensure the resolved path is still within the base directory
if let (Ok(canonical_final), Ok(canonical_base)) = (final_path.canonicalize(), base_dir.canonicalize()) {
if !canonical_final.starts_with(&canonical_base) {
return Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal detected: {} escapes base directory", self.relative_path),
});
}
}
Ok(final_path)
}
}
/// Enum to represent the source of an extension
#[derive(Debug, Clone)]
pub enum ExtensionSource {
/// Production extension installed in app data
Production {
path: PathBuf,
version: String,
},
/// Development mode extension with live reloading
Development {
dev_server_url: String,
manifest_path: PathBuf,
auto_reload: bool,
},
}
/// Complete extension data including source and runtime status
#[derive(Debug, Clone)]
pub struct Extension {
/// Unique extension ID
pub id: String,
/// Extension display name
pub name: String,
/// Source information (production or dev)
pub source: ExtensionSource,
/// Complete manifest data
pub manifest: ExtensionManifest,
/// Enabled status
pub enabled: bool,
/// Last access timestamp
pub last_accessed: SystemTime,
}
/// Cached permission data to avoid frequent database lookups
#[derive(Debug, Clone)]
pub struct CachedPermission {
/// The permissions that were fetched
pub permissions: Vec<DbExtensionPermission>,
/// When this cache entry was created
pub cached_at: SystemTime,
/// How long this cache entry is valid
pub ttl: Duration,
}
/// Enhanced extension manager with production/dev support and caching
#[derive(Default)]
pub struct ExtensionManager {
/// Production extensions loaded from app data directory
pub production_extensions: Mutex<HashMap<String, Extension>>,
/// Development mode extensions for live-reloading during development
pub dev_extensions: Mutex<HashMap<String, Extension>>,
/// Cache for extension permissions to improve performance
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
}
impl ExtensionManager {
/// Creates a new extension manager
pub fn new() -> Self {
Self {
production_extensions: Mutex::new(HashMap::new()),
dev_extensions: Mutex::new(HashMap::new()),
permission_cache: Mutex::new(HashMap::new()),
}
}
/// Adds a production extension to the manager
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
match &extension.source {
ExtensionSource::Production { .. } => {
let mut extensions = self.production_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
},
_ => Err(ExtensionError::ValidationError {
reason: "Expected Production source for production extension".to_string(),
})
}
}
/// Adds a development mode extension to the manager
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() {
return Err(ExtensionError::ValidationError {
reason: "Extension ID cannot be empty".to_string(),
});
}
match &extension.source {
ExtensionSource::Development { .. } => {
let mut extensions = self.dev_extensions.lock().unwrap();
extensions.insert(extension.id.clone(), extension);
Ok(())
},
_ => Err(ExtensionError::ValidationError {
reason: "Expected Development source for dev extension".to_string(),
})
}
}
/// Gets an extension by its ID
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
// First check development extensions (they take priority)
let dev_extensions = self.dev_extensions.lock().unwrap();
if let Some(extension) = dev_extensions.get(extension_id) {
return Some(extension.clone());
}
// Then check production extensions
let prod_extensions = self.production_extensions.lock().unwrap();
prod_extensions.get(extension_id).cloned()
}
/// Removes an extension from the manager
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
// Check dev extensions first
{
let mut dev_extensions = self.dev_extensions.lock().unwrap();
if dev_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
// Then check production extensions
{
let mut prod_extensions = self.production_extensions.lock().unwrap();
if prod_extensions.remove(extension_id).is_some() {
return Ok(());
}
}
Err(ExtensionError::NotFound {
id: extension_id.to_string(),
})
}
/// Gets cached permissions or indicates they need to be loaded
pub fn get_cached_permissions(
&self,
extension_id: &str,
resource: &str,
operation: &str,
) -> Option<Vec<DbExtensionPermission>> {
let cache = self.permission_cache.lock().unwrap();
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
if let Some(cached) = cache.get(&cache_key) {
let now = SystemTime::now();
if now.duration_since(cached.cached_at).unwrap_or(Duration::from_secs(0)) < cached.ttl {
return Some(cached.permissions.clone());
}
}
None
}
/// Updates the permission cache
pub fn update_permission_cache(
&self,
extension_id: &str,
resource: &str,
operation: &str,
permissions: Vec<DbExtensionPermission>,
) {
let mut cache = self.permission_cache.lock().unwrap();
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
cache.insert(cache_key, CachedPermission {
permissions,
cached_at: SystemTime::now(),
ttl: Duration::from_secs(60), // Cache for 60 seconds
});
}
/// Validates a manifest for security concerns
pub fn validate_manifest_security(&self, manifest: &ExtensionManifest) -> Result<(), ExtensionError> {
// Check for suspicious permission combinations
let has_filesystem = manifest.permissions.filesystem.is_some();
let has_database = manifest.permissions.database.is_some();
let has_shell = manifest.permissions.shell.is_some();
if has_filesystem && has_database && has_shell {
// This is a powerful combination, warn or check user confirmation elsewhere
}
// Validate ID format
if !manifest.id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
return Err(ExtensionError::ValidationError {
reason: "Invalid extension ID format. Must contain only alphanumeric characters, dash or underscore.".to_string()
});
}
Ok(())
}
/// Lists all enabled extensions (both dev and production)
pub fn list_enabled_extensions(&self) -> Vec<Extension> {
let mut extensions = Vec::new();
// Add enabled dev extensions first (higher priority)
{
let dev_extensions = self.dev_extensions.lock().unwrap();
extensions.extend(
dev_extensions
.values()
.filter(|ext| ext.enabled)
.cloned()
);
}
// Add enabled production extensions (avoiding duplicates)
{
let prod_extensions = self.production_extensions.lock().unwrap();
let dev_ids: std::collections::HashSet<String> = extensions.iter().map(|e| e.id.clone()).collect();
extensions.extend(
prod_extensions
.values()
.filter(|ext| ext.enabled && !dev_ids.contains(&ext.id))
.cloned()
);
}
extensions
}
}
// For backward compatibility - will be deprecated
#[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
}
impl ExtensionState {
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
let mut extensions = self.extensions.lock().unwrap();
extensions.insert(path, manifest);
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
let extensions = self.extensions.lock().unwrap();
extensions.values().find(|p| p.name == addon_id).cloned()
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DbExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource: String,
pub operation: String,
pub path: String,
}
/// Comprehensive error type for all extension-related operations
#[derive(Error, Debug)]
pub enum ExtensionError {
/// Security violation detected
#[error("Security violation: {reason}")]
SecurityViolation {
reason: String
},
/// Extension not found
#[error("Extension not found: {id}")]
NotFound {
id: String
},
/// Permission denied
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
PermissionDenied {
extension_id: String,
operation: String,
resource: String
},
/// IO error during extension operations
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error
},
/// Error during extension manifest parsing
#[error("Manifest error: {reason}")]
ManifestError {
reason: String
},
/// Input validation error
#[error("Validation error: {reason}")]
ValidationError {
reason: String
},
/// Development server error
#[error("Dev server error: {reason}")]
DevServerError {
reason: String
},
}
// For Tauri Command Serialization
impl serde::Serialize for ExtensionError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

View File

@ -0,0 +1,34 @@
<template>
<div class="w-full h-full overflow-scroll">
<div>
{{ iframeIndex }}
</div>
<iframe
v-if="iframeIndex"
ref="iFrameRef"
class="w-full h-full"
:src="iframeIndex"
sandbox="allow-scripts allow-same-origin"
allow="autoplay; speaker-selection; encrypted-media;"
/>
</div>
</template>
<script setup lang="ts">
definePageMeta({
name: 'haexExtension',
})
const { extensionEntry: iframeSrc } = storeToRefs(useExtensionsStore())
const iframeIndex = computed(() =>
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
)
</script>
<i18n lang="yaml">
de:
loading: Erweiterung wird geladen
en:
loading: Extension is loading
</i18n>

View File

@ -0,0 +1,256 @@
<template>
<div class="flex flex-col p-4 relative h-full">
<div
v-if="extensionStore.availableExtensions.length"
class="flex"
>
<UiButton
class="fixed top-20 right-4 btn-square btn-primary"
@click="prepareInstallExtensionAsync"
>
<Icon
name="mdi:plus"
size="1.5em"
/>
</UiButton>
<HaexExtensionCard
v-for="_extension in extensionStore.availableExtensions"
v-bind="_extension"
:key="_extension.id"
@remove="onShowRemoveDialog(_extension)"
/>
</div>
<div
v-else
class="h-full w-full"
>
<Icon
name="my-icon:extensions-overview"
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
/>
<div class="fixed top-30 right-10">
<UiTooltip :tooltip="t('extension.add')">
<UiButton
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
@click="prepareInstallExtensionAsync"
>
<Icon
name="mdi:plus"
size="1.5em"
class="rotate-45"
/>
</UiButton>
</UiTooltip>
</div>
</div>
<HaexExtensionDialogReinstall
v-model:open="openOverwriteDialog"
:manifest="extension.manifest"
@confirm="addExtensionAsync"
/>
<HaexExtensionDialogInstall
v-model:open="showConfirmation"
:manifest="extension.manifest"
@confirm="addExtensionAsync"
/>
<HaexExtensionDialogRemove
v-model:open="showRemoveDialog"
:extension="extensionToBeRemoved"
@confirm="removeExtensionAsync"
/>
</div>
</template>
<script setup lang="ts">
import { join } from '@tauri-apps/api/path'
import { open } from '@tauri-apps/plugin-dialog'
import { readTextFile } from '@tauri-apps/plugin-fs'
import type {
IHaexHubExtension,
IHaexHubExtensionManifest,
} from '~/types/haexhub'
definePageMeta({
name: 'extensionOverview',
})
const { t } = useI18n()
const extensionStore = useExtensionsStore()
const showConfirmation = ref(false)
const openOverwriteDialog = ref(false)
const extension = reactive<{
manifest: IHaexHubExtensionManifest | null | undefined
path: string | null
}>({
manifest: null,
path: '',
})
const loadExtensionManifestAsync = async () => {
try {
extension.path = await open({ directory: true, recursive: true })
if (!extension.path) return
const manifestFile = JSON.parse(
await readTextFile(await join(extension.path, 'manifest.json')),
)
if (!extensionStore.checkManifest(manifestFile))
throw new Error(`Manifest fehlerhaft ${JSON.stringify(manifestFile)}`)
return manifestFile
} catch (error) {
console.error('Fehler loadExtensionManifestAsync:', error)
add({ color: 'error', description: JSON.stringify(error) })
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
}
}
const { add } = useToast()
const { addNotificationAsync } = useNotificationStore()
const prepareInstallExtensionAsync = async () => {
try {
const manifest = await loadExtensionManifestAsync()
if (!manifest) throw new Error('No valid Manifest found')
extension.manifest = manifest
const isAlreadyInstalled = await extensionStore.isExtensionInstalledAsync({
id: manifest.id,
version: manifest.version,
})
if (isAlreadyInstalled) {
openOverwriteDialog.value = true
} else {
await addExtensionAsync()
}
} catch (error) {
add({ color: 'error', description: JSON.stringify(error) })
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
}
}
const addExtensionAsync = async () => {
try {
await extensionStore.installAsync(extension.path)
await extensionStore.loadExtensionsAsync()
add({
color: 'success',
title: t('extension.success.title', {
extension: extension.manifest?.name,
}),
description: t('extension.success.text'),
})
await addNotificationAsync({
text: t('extension.success.text'),
type: 'success',
title: t('extension.success.title', {
extension: extension.manifest?.name,
}),
})
} catch (error) {
console.error('Fehler addExtensionAsync:', error)
add({ color: 'error', description: JSON.stringify(error) })
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
}
}
const showRemoveDialog = ref(false)
const extensionToBeRemoved = ref<IHaexHubExtension>()
const onShowRemoveDialog = (extension: IHaexHubExtension) => {
extensionToBeRemoved.value = extension
showRemoveDialog.value = true
}
const removeExtensionAsync = async () => {
if (!extensionToBeRemoved.value?.id || !extensionToBeRemoved.value?.version) {
add({
color: 'error',
description: 'Erweiterung kann nicht gelöscht werden',
})
return
}
try {
await extensionStore.removeExtensionAsync(
extensionToBeRemoved.value.id,
extensionToBeRemoved.value.version,
)
await extensionStore.loadExtensionsAsync()
add({
color: 'success',
title: t('extension.remove.success.title', {
extensionName: extensionToBeRemoved.value.name,
}),
description: t('extension.remove.success.text', {
extensionName: extensionToBeRemoved.value.name,
}),
})
await addNotificationAsync({
text: t('extension.remove.success.text', {
extensionName: extensionToBeRemoved.value.name,
}),
type: 'success',
title: t('extension.remove.success.title', {
extensionName: extensionToBeRemoved.value.name,
}),
})
} catch (error) {
add({
color: 'error',
title: t('extension.remove.error.title'),
description: t('extension.remove.error.text', {
error: JSON.stringify(error),
}),
})
await addNotificationAsync({
type: 'error',
title: t('extension.remove.error.title'),
text: t('extension.remove.error.text', { error: JSON.stringify(error) }),
})
}
}
</script>
<i18n lang="yaml">
de:
title: 'Erweiterung installieren'
extension:
remove:
success:
text: 'Erweiterung {extensionName} wurde erfolgreich entfernt'
title: '{extensionName} entfernt'
error:
text: "Erweiterung {extensionName} konnte nicht entfernt werden. \n {error}"
title: 'Fehler beim Entfernen von {extensionName}'
add: 'Erweiterung hinzufügen'
success:
title: '{extension} hinzugefügt'
text: 'Die Erweiterung wurde erfolgreich hinzugefügt'
en:
title: 'Install extension'
extension:
remove:
success:
text: 'Extension {extensionName} was removed'
title: '{extensionName} removed'
error:
text: "Extension {extensionName} couldn't be removed. \n {error}"
title: 'Exception during uninstall {extensionName}'
add: 'Add Extension'
success:
title: '{extension} added'
text: 'Extensions was added successfully'
</i18n>

View File

@ -37,13 +37,14 @@ export const usePasswordGroupStore = defineStore('passwordGroupStore', () => {
) => {
const group = groups.value.find((group) => group.id === groupId)
console.log('getParentChain1: found group', group, chain)
if (group) {
/* if (group) {
chain.push(group)
console.log('getParentChain: found group', group, chain)
return getParentChain(group.parentId, chain)
}
return chain.reverse()
return chain.reverse() */
return []
}
const syncGroupItemsAsync = async () => {
@ -322,7 +323,7 @@ const deleteGroupAsync = async (groupId: string, final: boolean = false) => {
const items = (await readByGroupIdAsync(groupId)) ?? []
console.log('deleteGroupAsync delete Items', items)
for (const item of items) {
await deleteAsync(item.id, true)
if (item) await deleteAsync(item.id, true)
}
return await currentVault?.drizzle

View File

@ -33,7 +33,6 @@ export const usePasswordItemStore = defineStore('passwordItemStore', () => {
const syncItemsAsync = async () => {
const { currentVault } = useVaultStore()
items.value =
(await currentVault?.drizzle
.select()