From d0258198880c4a62befdac4e2dcfeead973ba3d5 Mon Sep 17 00:00:00 2001 From: haex Date: Fri, 26 Sep 2025 15:35:54 +0200 Subject: [PATCH] refactored permission system and error handling --- .npmrc | 1 + README.md | 21 + package.json | 2 +- pnpm-lock.yaml | 351 ++++++++++---- src-tauri/src/crdt/hlc.rs | 18 +- src-tauri/src/crdt/transformer.rs | 24 +- src-tauri/src/database/core.rs | 5 +- src-tauri/src/database/error.rs | 76 ++- src-tauri/src/database/mod.rs | 61 ++- src-tauri/src/extension/core.rs | 168 ++++++- src-tauri/src/extension/database/mod.rs | 86 ++-- .../src/extension/database/permissions.rs | 125 ++--- src-tauri/src/extension/error.rs | 214 +++++++++ src-tauri/src/extension/filesystem/core.rs | 186 ++++++++ src-tauri/src/extension/filesystem/mod.rs | 2 + .../src/extension/filesystem/permissions.rs | 101 ++++ src-tauri/src/extension/mod.rs | 3 + src-tauri/src/extension/permission_manager.rs | 297 ++++++++++++ src-tauri/src/lib.rs | 21 +- src-tauri/src/models.rs | 77 ++- src-tauri/src/models_final.rs | 34 ++ src-tauri/src/models_new.rs | 441 ++++++++++++++++++ .../[vaultId]/extensions/[extensionId].vue | 34 ++ .../vault/[vaultId]/extensions/index.vue | 256 ++++++++++ src/stores/passwords/groups.ts | 7 +- src/stores/passwords/items.ts | 1 - 26 files changed, 2312 insertions(+), 300 deletions(-) create mode 100644 .npmrc create mode 100644 src-tauri/src/extension/error.rs create mode 100644 src-tauri/src/extension/filesystem/core.rs create mode 100644 src-tauri/src/extension/filesystem/mod.rs create mode 100644 src-tauri/src/extension/filesystem/permissions.rs create mode 100644 src-tauri/src/extension/permission_manager.rs create mode 100644 src-tauri/src/models_final.rs create mode 100644 src-tauri/src/models_new.rs create mode 100644 src/pages/vault/[vaultId]/extensions/[extensionId].vue create mode 100644 src/pages/vault/[vaultId]/extensions/index.vue diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c483022 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true \ No newline at end of file diff --git a/README.md b/README.md index eaf1bff..faf2e67 100644 --- a/README.md +++ b/README.md @@ -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` ``` diff --git a/package.json b/package.json index f91ac76..d9d4a6d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d69bc3c..63cd07e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src-tauri/src/crdt/hlc.rs b/src-tauri/src/crdt/hlc.rs index d1588a8..1722217 100644 --- a/src-tauri/src/crdt/hlc.rs +++ b/src-tauri/src/crdt/hlc.rs @@ -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 { + pub fn try_initialize(conn: &Connection, app_handle: &AppHandle) -> Result { // 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. diff --git a/src-tauri/src/crdt/transformer.rs b/src-tauri/src/crdt/transformer.rs index d315531..c6dfb6d 100644 --- a/src-tauri/src/crdt/transformer.rs +++ b/src-tauri/src/crdt/transformer.rs @@ -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(), }); } } diff --git a/src-tauri/src/database/core.rs b/src-tauri/src/database/core.rs index 8caeae6..deefe1b 100644 --- a/src-tauri/src/database/core.rs +++ b/src-tauri/src/database/core.rs @@ -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(), }); } diff --git a/src-tauri/src/database/error.rs b/src-tauri/src/database/error.rs index 9d8c5a1..2cfd204 100644 --- a/src-tauri/src/database/error.rs +++ b/src-tauri/src/database/error.rs @@ -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, resource: Option, reason: String, }, + #[error("Query error: {reason}")] QueryError { reason: String }, @@ -111,7 +119,43 @@ impl From for DatabaseError { } } -impl From 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 for DatabaseError { fn from(err: crate::extension::database::ExtensionDatabaseError) -> Self { match err { crate::extension::database::ExtensionDatabaseError::Permission { source } => { @@ -156,4 +200,4 @@ impl From for DatabaseError } } } -} +} */ diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 7106ebd..7aa9533 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -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>>); -pub struct AppState { - pub db: DbConnection, - pub hlc: Mutex, // 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 { + 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(()) +} diff --git a/src-tauri/src/extension/core.rs b/src-tauri/src/extension/core.rs index d90dcd9..d4b6754 100644 --- a/src-tauri/src/extension/core.rs +++ b/src-tauri/src/extension/core.rs @@ -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, + pub entry: String, + pub icon: Option, + pub permissions: ExtensionPermissions, + pub homepage: Option, + pub description: Option, +} + +/// 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, + pub cached_at: SystemTime, + pub ttl: Duration, +} + +/// Enhanced extension manager +#[derive(Default)] +pub struct ExtensionManager { + pub production_extensions: Mutex>, + pub dev_extensions: Mutex>, + pub permission_cache: Mutex>, +} + +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 { + // 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>, +} + +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 { + let extensions = self.extensions.lock().unwrap(); + extensions.values().find(|p| p.name == addon_id).cloned() + } +} + #[derive(Deserialize, Debug)] struct ExtensionInfo { id: String, diff --git a/src-tauri/src/extension/database/mod.rs b/src-tauri/src/extension/database/mod.rs index 6910c33..c2b9211 100644 --- a/src-tauri/src/extension/database/mod.rs +++ b/src-tauri/src/extension/database/mod.rs @@ -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(&self, serializer: S) -> Result - 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, ExtensionDatabaseError> { +) -> Result, 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, extension_id: String, state: State<'_, AppState>, -) -> Result, ExtensionDatabaseError> { +) -> Result, 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(), }); } diff --git a/src-tauri/src/extension/database/permissions.rs b/src-tauri/src/extension/database/permissions.rs index 0214fbf..ae1f119 100644 --- a/src-tauri/src/extension/database/permissions.rs +++ b/src-tauri/src/extension/database/permissions.rs @@ -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 = 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 { +) -> Result { 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 { +) -> Result { 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 { +) -> Result { 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"), } - } + } */ } diff --git a/src-tauri/src/extension/error.rs b/src-tauri/src/extension/error.rs new file mode 100644 index 0000000..9bffeae --- /dev/null +++ b/src-tauri/src/extension/error.rs @@ -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>, + }, + + #[error("Shell command failed: {reason}")] + Shell { + reason: String, + exit_code: Option, + }, + + /* #[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, + ) -> Self { + Self::Http { + reason: reason.to_string(), + source: Some(source), + } + } + + /// Convenience constructor for shell errors + pub fn shell_error(reason: &str, exit_code: Option) -> 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(&self, serializer: S) -> Result + 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::::None)?; + } + + state.end() + } +} + +// For Tauri command serialization +impl From 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")); + } +} diff --git a/src-tauri/src/extension/filesystem/core.rs b/src-tauri/src/extension/filesystem/core.rs new file mode 100644 index 0000000..90c93e1 --- /dev/null +++ b/src-tauri/src/extension/filesystem/core.rs @@ -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>, + /// Write access to files and directories (includes create/delete) + /// Examples: ["$APPDATA/my-extension/**", "$DOWNLOAD/*.pdf"] + pub write: Option>, +} + +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())) +} diff --git a/src-tauri/src/extension/filesystem/mod.rs b/src-tauri/src/extension/filesystem/mod.rs new file mode 100644 index 0000000..97613be --- /dev/null +++ b/src-tauri/src/extension/filesystem/mod.rs @@ -0,0 +1,2 @@ +pub mod core; +pub mod permissions; diff --git a/src-tauri/src/extension/filesystem/permissions.rs b/src-tauri/src/extension/filesystem/permissions.rs new file mode 100644 index 0000000..3a07613 --- /dev/null +++ b/src-tauri/src/extension/filesystem/permissions.rs @@ -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>, + /// Write access to files and directories (includes create/delete) + pub write: Option>, +} + +/// 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 { + /* 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()) + } +} diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 2055695..5c9c3e2 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -1,2 +1,5 @@ pub mod core; pub mod database; +pub mod error; +pub mod filesystem; +pub mod permission_manager; diff --git a/src-tauri/src/extension/permission_manager.rs b/src-tauri/src/extension/permission_manager.rs new file mode 100644 index 0000000..b4c9383 --- /dev/null +++ b/src-tauri/src/extension/permission_manager.rs @@ -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, + pub filesystem: Vec, + pub http: Vec, + pub shell: Vec, +} + +#[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, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ShellPermission { + pub extension_id: String, + pub command: String, + pub arguments: Vec, +} + +/// 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, 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, 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, ExtensionError> { + // Implementierung für HTTP-Permissions + todo!("Implementiere HTTP-Permission-Loading") + } + + async fn get_shell_permissions( + connection: &DbConnection, + extension_id: &str, + ) -> Result, 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")); + } +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8af1612..d6fc533 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, // 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 { diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 4b43f2f..17aa3cf 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -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, pub entry: String, + pub icon: Option, pub permissions: ExtensionPermissions, + pub homepage: Option, + pub description: Option, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct ExtensionPermissions { pub database: Option, pub http: Option>, pub filesystem: Option, } -#[derive(Serialize, Deserialize, Clone)] -pub struct DatabasePermissions { - pub read: Option>, - pub write: Option>, - pub create: Option>, -} +/// 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>, @@ -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 }, +} + */ diff --git a/src-tauri/src/models_final.rs b/src-tauri/src/models_final.rs new file mode 100644 index 0000000..1ff24ca --- /dev/null +++ b/src-tauri/src/models_final.rs @@ -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); + } +} \ No newline at end of file diff --git a/src-tauri/src/models_new.rs b/src-tauri/src/models_new.rs new file mode 100644 index 0000000..0012cd7 --- /dev/null +++ b/src-tauri/src/models_new.rs @@ -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, + pub entry: String, + pub icon: Option, + pub permissions: ExtensionPermissions, + pub homepage: Option, + pub description: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ExtensionPermissions { + pub database: Option, + pub http: Option>, + pub filesystem: Option, + pub shell: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DatabasePermissions { + pub read: Option>, + pub write: Option>, + pub create: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FilesystemPermissions { + /// Read access to files and directories + pub read: Option>, + /// Write access to files and directories (includes create/delete) + pub write: Option>, +} + +/// 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 { + 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, + /// 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>, + /// Development mode extensions for live-reloading during development + pub dev_extensions: Mutex>, + /// Cache for extension permissions to improve performance + pub permission_cache: Mutex>, +} + +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 { + // 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> { + 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, + ) { + 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 { + 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 = 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>, +} + +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 { + 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(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} \ No newline at end of file diff --git a/src/pages/vault/[vaultId]/extensions/[extensionId].vue b/src/pages/vault/[vaultId]/extensions/[extensionId].vue new file mode 100644 index 0000000..af300a4 --- /dev/null +++ b/src/pages/vault/[vaultId]/extensions/[extensionId].vue @@ -0,0 +1,34 @@ +