From 033c9135c645b45df0ff4a96efd45426f923750c Mon Sep 17 00:00:00 2001 From: haex Date: Wed, 15 Oct 2025 21:54:50 +0200 Subject: [PATCH] removed haex-pass components --- README.md | 193 ++-- src-tauri/Cargo.lock | 12 +- src-tauri/Cargo.toml | 4 +- src-tauri/bindings/ExtensionInfoResponse.ts | 2 +- src-tauri/bindings/ExtensionManifest.ts | 2 +- src-tauri/bindings/ExtensionPreview.ts | 2 +- .../migrations/0002_amazing_iron_fist.sql | 22 + .../migrations/meta/0002_snapshot.json | 930 ++++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + src-tauri/database/schemas/haex.ts | 46 +- src-tauri/database/vault.db | Bin 131072 -> 139264 bytes .../app/src/main/assets/database/vault.db | Bin 131072 -> 139264 bytes src-tauri/src/database/generated.rs | 42 +- src-tauri/src/extension/core/manager.rs | 234 ++--- src-tauri/src/extension/core/manifest.rs | 55 +- src-tauri/src/extension/core/protocol.rs | 218 +++- src-tauri/src/extension/core/types.rs | 6 +- src-tauri/src/extension/database/mod.rs | 28 +- src-tauri/src/extension/error.rs | 4 +- src-tauri/src/extension/mod.rs | 259 ++++- .../src/extension/permissions/validator.rs | 8 + src-tauri/src/lib.rs | 4 +- .../haex/extension/marketplace-card.vue | 46 +- src/components/haex/pass/card/group.vue | 107 -- src/components/haex/pass/card/index.vue | 12 - .../haex/pass/dialog/deleteItem.vue | 46 - .../haex/pass/dialog/unsavedChanges.vue | 51 - .../haex/pass/group/breadcrumbs.vue | 59 -- src/components/haex/pass/group/composables.ts | 71 -- src/components/haex/pass/group/index.vue | 106 -- src/components/haex/pass/item/details.vue | 127 --- src/components/haex/pass/item/history.vue | 118 --- src/components/haex/pass/item/index.vue | 182 ---- src/components/haex/pass/item/keyValue.vue | 126 --- src/components/haex/pass/menu/bottom.vue | 92 -- .../haex/pass/mobile/menu/index.vue | 128 --- src/components/haex/pass/mobile/menu/item.vue | 39 - .../haex/pass/mobile/menu/types.d.ts | 7 - src/components/ui/dropdown/vault.vue | 7 + src/composables/extensionMessageHandler.ts | 146 ++- .../[vaultId]/extensions/[extensionId].vue | 190 +++- .../vault/[vaultId]/extensions/index.vue | 73 +- src/pages/vault/[vaultId]/passwords.vue | 11 - .../passwords/[[groupId]]/create.vue | 108 -- .../[vaultId]/passwords/[[groupId]]/edit.vue | 177 ---- .../[vaultId]/passwords/[[groupId]]/index.vue | 273 ----- .../passwords/[[groupId]]/item/[itemId].vue | 226 ----- .../passwords/[[groupId]]/item/create.vue | 159 --- src/pages/vault/[vaultId]/settings.vue | 65 +- .../vault/[vaultId]/settings/developer.vue | 279 ++++++ src/plugins/plugins/i18n.client.ts | 9 - src/stores/extensions/index.ts | 85 +- src/stores/extensions/tabs.ts | 12 +- src/stores/passwords/actionMenu/de.json | 8 - src/stores/passwords/actionMenu/en.json | 8 - src/stores/passwords/actionMenu/index.ts | 56 -- src/stores/passwords/groups.ts | 353 ------- src/stores/passwords/history.ts | 28 - src/stores/passwords/items.ts | 362 ------- src/stores/vault/device.ts | 8 +- src/types/haexhub.d.ts | 39 +- src/utils/extension.ts | 58 ++ src/utils/helper.ts | 22 +- todos.md | 4 + 64 files changed, 2502 insertions(+), 3659 deletions(-) create mode 100644 src-tauri/database/migrations/0002_amazing_iron_fist.sql create mode 100644 src-tauri/database/migrations/meta/0002_snapshot.json delete mode 100644 src/components/haex/pass/card/group.vue delete mode 100644 src/components/haex/pass/card/index.vue delete mode 100644 src/components/haex/pass/dialog/deleteItem.vue delete mode 100644 src/components/haex/pass/dialog/unsavedChanges.vue delete mode 100644 src/components/haex/pass/group/breadcrumbs.vue delete mode 100644 src/components/haex/pass/group/composables.ts delete mode 100644 src/components/haex/pass/group/index.vue delete mode 100644 src/components/haex/pass/item/details.vue delete mode 100644 src/components/haex/pass/item/history.vue delete mode 100644 src/components/haex/pass/item/index.vue delete mode 100644 src/components/haex/pass/item/keyValue.vue delete mode 100644 src/components/haex/pass/menu/bottom.vue delete mode 100644 src/components/haex/pass/mobile/menu/index.vue delete mode 100644 src/components/haex/pass/mobile/menu/item.vue delete mode 100644 src/components/haex/pass/mobile/menu/types.d.ts delete mode 100644 src/pages/vault/[vaultId]/passwords.vue delete mode 100644 src/pages/vault/[vaultId]/passwords/[[groupId]]/create.vue delete mode 100644 src/pages/vault/[vaultId]/passwords/[[groupId]]/edit.vue delete mode 100644 src/pages/vault/[vaultId]/passwords/[[groupId]]/index.vue delete mode 100644 src/pages/vault/[vaultId]/passwords/[[groupId]]/item/[itemId].vue delete mode 100644 src/pages/vault/[vaultId]/passwords/[[groupId]]/item/create.vue create mode 100644 src/pages/vault/[vaultId]/settings/developer.vue delete mode 100644 src/plugins/plugins/i18n.client.ts delete mode 100644 src/stores/passwords/actionMenu/de.json delete mode 100644 src/stores/passwords/actionMenu/en.json delete mode 100644 src/stores/passwords/actionMenu/index.ts delete mode 100644 src/stores/passwords/groups.ts delete mode 100644 src/stores/passwords/history.ts delete mode 100644 src/stores/passwords/items.ts create mode 100644 src/utils/extension.ts create mode 100644 todos.md diff --git a/README.md b/README.md index dd0c95c..76b4071 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,144 @@ -# HaexHub - The European "Everything App" +# 🧩 HaexHub – The European “Everything App” -## Vision +## 🌍 Vision -Today, we undoubtedly find ourselves in the computer age. Almost everyone owns at least one computer, often even more. Most probably have at least a smartphone and a standard PC. On each of these devices (Desktop PC, Laptop, Tablet, Smartphone) and systems (Windows, macOS, Linux (all flavors), Android, iOS), there are various programs and data, which can be highly individual and sensitive. Unfortunately, interoperability between these devices and systems often proves difficult, sometimes even impossible, for a multitude of reasons. On one hand, there are the system providers themselves (like Microsoft, Apple, Google), who often design their systems to make it as easy as possible for users to enter their ecosystems, but place many hurdles in the way when users wish to leave again. The golden cage, as we say in Germany, or walled garden. However, it's not just the system providers per se who make cross-device and cross-system work difficult. Another problem lies with the software manufacturers/providers. Since it is already challenging and above all resource-intensive (time, money, and technical know-how) to provide a good and "secure" product for one device class and/or system, it's not uncommon for a program to be developed (initially) for only one platform. So, there might be a program for Windows or Apple, but not for Linux, or only in one distribution/package format. Or there might be an app for iOS and/or Android, but not for the PC. This is partly due to the fact that it would simply be too complex to develop and, especially, maintain a product for multiple systems and devices (simultaneously). This effort is almost insurmountable, particularly for startups, small businesses, and individual open-source developers working on their passion projects in their spare time. -Let's not even start talking about application distribution. For each platform, you end up with a separate build pipeline that builds, tests, signs, packages the application into the appropriate format (msi, exe, deb, flatpak, snap, AppImage, Apk, etc.), and delivers it to the corresponding store (AppStore, PlayStore, Windows Store, and the various repositories of Linux distributions). This is a huge cascade of tasks that especially causes problems for small companies (at least if you want to serve ALL platforms simultaneously). -Wouldn't it be nice if there were a simple(r) way for developers to develop and build their application just once and then be able to serve ALL\* devices and systems? PWAs were already on the right track, but there is often a lack of more in-depth access to system resources, such as file or console access. -HaexHub gives any web application/PWA superpowers. -Extensions can be used to add any functions to HaexHub, whereby almost any access to the underlying system is possible, provided that the necessary authorizations have been granted by the user beforehand. +We are living in the **computer age** — nearly everyone owns multiple devices: a smartphone, a laptop, perhaps even a desktop PC or tablet. +Each of these runs its own **operating system** — Windows, macOS, Linux, Android, iOS — and hosts a unique mix of **apps and data**. -\*In principle, the approach presented here allows an application to run on all devices and systems. However, some applications might still only be usable on certain devices or systems. For example, if an application absolutely requires an NFC device, which is typically not found on a desktop PC, then this application will probably only work on mobile devices. Or if an application requires system-specific interfaces or programs, such as the Registry on Windows or systemd on Linux, then this application will naturally only work where these dependencies are found. However, developers who create their applications without such dependencies can immediately serve all devices and systems. +Unfortunately, **interoperability** between these devices is often poor or even impossible. +The reasons are many: -## Enter HaexHub +- **Platform lock-in**: Vendors like Microsoft, Apple, or Google design systems that make it easy to _enter_ their ecosystem but difficult to _leave_. +- **Fragmented software development**: Developers face high technical and financial hurdles to support multiple platforms at once. -HaexHub provides a framework that makes it incredibly easy for the community and any developer to build extensions (web applications), which can then be easily integrated into HaexHub by users. Each extension is essentially a web application that can be loaded, executed, customized, and deleted at runtime. Each extension is confined within an IFrame, communicating with HaexHub via APIs using postMessage. HaexHub, in turn, checks these requests for the necessary permissions, executes or rejects the command, and returns a possible response to the caller – ideally, the correct result. -Since these are purely web applications, they are initially subject to the same limitations as any other web application in any "normal" browser worldwide. Fun Fact: Extensions in HaexHub are even more restricted than that. While a "normal" web application can, for example, load additional resources (JavaScript, CSS, images, ads) (assuming CORS allows it), this is initially not possible with a HaexHub extension. Everything the extension needs to be able to do must be specified as a permission in a manifest and approved by the user before (potentially) dangerous actions are executed on the host. And loading external resources is already considered such a risk from Tauri's (and my) perspective, as it can severely compromise the user's privacy. -With the appropriate permissions, however, an extension can do almost anything possible on a computer. Thus, unlike a "normal" web application, an extension can directly access the host's file system, execute other applications and commands, make/manipulate/block web requests, or access the SQLite database. To use these interfaces, each extension must declare the corresponding permissions in a manifest, which must then be approved by the user. Otherwise, no access to the host system is possible. Extensions can be added and removed at runtime. Since the extension runs in an IFrame, it cannot cause much damage without the appropriate permissions. It would be a pure web application where routing within the application is possible (WebHistoryHash). However, as soon as it tries to load external resources, regardless of whether they are local from the host or from any server on the World Wide Web, the extension is on its own without permission. -Technically, for example, it would pose no problem to make the host system's shell available to extensions. This could give Visual Studio Code in the browser superpowers. While a web version of Visual Studio Code already exists, its usability is limited. For instance, it's not possible to directly access the shell or the file system, which significantly hinders file management. And since no commands or applications can be executed on the host, it's (unfortunately) practically useless for developers. Visual Studio Code as a HaexHub extension could be used like a native application. And thanks to HaexHub's permission concept, it can be controlled with fine granularity which extension is allowed to execute what and how, and what is not. An extension with such power over the host, which can be both advantageous and disadvantageous for the user, should naturally be handled with particular care. It would probably not be a good idea to grant this permission to any advertising and data tracking services. +Creating and maintaining one secure, high-quality app for _all_ systems can be almost impossible — especially for small teams, startups, and indie developers. -The framework itself provides a platform that will be available on all common devices (Desktop PC, Laptop, Tablet, Smartphone) and systems (Windows, macOS, Linux (all flavors), Android, iOS). All extensions can then be used on all supported devices and systems (provided there are no dependencies in the extension that are only available on specific devices or systems, like NFC, Google Pay, etc.). -All user and extension data can be securely stored and used in the locally encrypted SQLite database. To enable comfortable use of the database across multiple devices and systems, there will be a synchronization server that allows the database to be synchronized conflict-free across devices and systems. This server can, of course, also be self-hosted, ensuring the user is never dependent on a single provider. -Furthermore, the data can be encrypted beforehand, making it unreadable by third parties. +And then there’s **distribution**: each platform requires its own build, packaging, signing, and publishing process. +What if you could build your app **once** and deploy it **everywhere**? -HaexHub is a cross-platform, local-first, open-source application that prioritizes user privacy, security, and digital sovereignty. The goal is for the user to have control over their data at all times and be able to independently decide what they want to disclose to whom. Additionally, they should be able to adjust this decision at any time. -Through the possibility of extensions, HaexHub is also almost infinitely expandable. What Visual Studio Code is for text editors/IDEs, HaexHub will be for (web) applications and even has the potential to become the European counterpart to WeChat (the "everything app"). However, without a central authority controlling everything. +> **HaexHub** makes that possible — giving every web app or PWA **superpowers**. -But first things first. +With HaexHub, developers can extend functionality via **extensions** that run securely inside the app, with carefully controlled permissions for accessing system features (files, shell, database, etc.). -## Technical Foundations +--- -The technical foundation of the project is Tauri. This framework makes it possible to provide native applications for all common devices (Desktops, Laptops, Tablets, Smartphones) and systems (Windows, Linux, macOS, Android, iOS) with the same codebase. Tauri is comparable to Electron (the technical basis for Visual Studio Code, for example), but the applications created with it are significantly smaller because Tauri uses the native rendering engine of the respective platform (WebView2 (Windows), WKWebView (macOS), WebKitGTK (Linux)) and does not bundle a (customized Chromium) browser, as is the case with Electron. Furthermore, Tauri offers significant advantages over Electron in terms of security and resource efficiency. There is also a sophisticated permission system, which effectively shields the frontend from the host. All access to the host system is only possible with the appropriate permission. This permission concept is also used for the (HaexHub) extensions, thereby ensuring the security of third-party extensions as well. +## 🚀 Enter HaexHub -The project follows a strict local-first approach. This means that HaexHub can fundamentally be used without any form of online account or internet access. The extensions are also stored locally and can be used offline, provided, of course, that the extension itself can function without the internet. A messenger extension will likely make limited sense without internet access. An image viewer or text editor, however, should work fine without the internet. -All user data can be persistently stored and used in a locally encrypted SQLite database, even across extensions, with the appropriate permissions, of course. Unlike many other applications that call themselves local-first, this project implements this approach more consistently. Most applications claiming to be local-first often aren't truly so. The data usually resides (unencrypted) on a backend server and is merely "cached" to varying degrees in the frontend. While this allows these applications to be used offline for a while, the usage is either restricted (read-only in Bitwarden, for example) or the persistence is temporary at best. Most approaches, like this project, use an SQLite (or similar) database in the frontend to achieve offline capability, but this is usually implemented in a browser via IndexedDB or OPFS. Examples include [powersync](https://www.powersync.com/) , [evolu](https://www.evolu.dev/), or [electricSql](https://electric-sql.com/). The problem here is that such persistence is never truly permanent, as the operating system and/or browser can decide when to free up storage. For instance, it's common for Apple to clear the storage of web applications that haven't been used for over a week. As long as the user's data is still present in the backend, this is only moderately tragic, as the "source of truth" residing there can be synchronized back to the frontend at any time. However, this always requires an online account and internet access. Furthermore, with these approaches, the user cannot simply copy their data onto a USB stick and take it with them to use on a completely different computer (perhaps where only intranet is available). -Moreover, all these approaches are subject to the limitations of the respective browser. The limitation on persistent storage is particularly noteworthy here. All browsers have strict limits, which is why this approach is not suitable for all requirements. Since HaexHub stores data not in the browser, but in a real SQLite database on the hard drive, it is only subject to the hardware limitations of the host system (or USB stick/storage medium). +HaexHub provides a **framework** for building and running modular, sandboxed **web extensions** — web apps that run in an isolated environment but can communicate securely with the host. -With HaexHub, all user and extension data can be permanently stored in the local and encrypted database without requiring an online account. However, to make the user's data conveniently and securely available on multiple devices, there will be a synchronization service to synchronize the database state across the user's various devices and systems. The user can, of course, also host this service themselves on their (local) systems or servers. The database state is thus temporarily stored on a (third-party) server and can be synchronized from there with other instances of the local SQLite database. To further enhance data security, the user can also encrypt the data before sending it to the backend, making it unreadable by third parties. This will likely be enabled by default, but it can also be turned off, as there are legitimate use cases where it might be disadvantageous or undesirable. Particularly in corporate or government environments, it could be problematic if all user (employee) data were stored encrypted on the company servers. If the employee becomes unavailable (resignation, accident, death) and their database password (or the encryption key stored in the database) is unknown, there would be no way to access this data. -Since this use case should also be considered, backend encryption will be optional. +Each extension: -As HaexHub is ultimately a kind of distributed and federated system, there is no (single) authority that could control everything. Unless the user truly has only one instance of their database (perhaps on a USB stick) and always carries it with them. Part of HaexHub's charm, however, is that the user can have multiple instances of their SQLite database on multiple devices and systems without having to worry about how the correct data (source of truth) gets from A to B and B to A. -To make this possible and to synchronize even conflicting data states of the SQLite database, HaexHub uses Conflict-free Replicated Data Types (CRDTs). This will make it possible to merge multiple conflicting data states, even if they are encrypted. +- Runs inside an **IFrame**. +- Uses **postMessage APIs** to communicate with HaexHub. +- Declares required **permissions** in a manifest file. +- Can be added or removed at runtime. -## Extensions +Without explicit permission, extensions cannot access the file system, network, or external resources — ensuring **privacy and security** by default. +Once granted, however, extensions can unlock full desktop-like capabilities: +access files, execute commands, or interact with SQLite databases. -The real highlight of HaexHub, however, lies in its extensions. All end-user functionality will ultimately be provided through extensions. There will be (official/core) extensions and third-party extensions. One of the first (official) extensions will be a password manager, for example, but a file synchronization service is also planned. -Each extension is essentially just a web application\* loaded into an IFrame. This keeps all extensions well isolated (sandboxed) from the main application (HaexHub) and the user's host system, ensuring the user's security and privacy. Of course, as with any application, a degree of trust must be placed in the extension developer that they are genuinely only doing what they claim to do. HaexHub is ingenious, but it can't perform magic. -Each extension must declare the permissions it requires in a manifest, which must then be accepted by the user. This ensures that each extension can only access the resources (file system, web requests, database access, etc.) for which it has the appropriate permissions. +Imagine a **web-based VS Code** that can directly access your local shell and file system — something that current web IDEs can’t do. +With HaexHub’s permission model, such power is possible, but **always under user control**. -In principle, any (existing) web application could be integrated and run within HaexHub. Technically, each extension is just a web application, but with significantly more capabilities. Traditional web applications are restricted by the (justified) limitations of a browser. For example, a web application cannot simply access the host system's file system or manipulate network traffic. And for good reasons. With HaexHub, however, these limitations do not exist. A (HaexHub) extension can indeed access the file system if it has the corresponding permission. This opens up almost unlimited application possibilities, making the term "everything app" seem not so far-fetched. In a future iteration, a browser and later a payment option (GNU Taler?!) are planned to be added, so it could truly become a fully-fledged counterpart to WeChat. However, these aspects are not considered in the first iteration of the application. -By providing extensions, HaexHub can truly be enhanced arbitrarily. Extension developers could use simple tools (Vite application) to immediately provide their functionality for all devices and systems and utilize the provided ecosystem, without the developer having to deal with the peculiarities of each system for development and distribution. (Provided, of course, they don't rely on dependencies that only exist on specific systems or devices). -Extensions can also access the data of other extensions (e.g., via the SQLite database) and build upon it (with appropriate permission, naturally). -I want to outline this with a concrete example. The first official extension will be a password manager. -This will be a Nuxt/Vue application. The password manager's manifest will request permission to create a few tables and to read from and write to them. The extension then provides a nice UI for creating and managing login credentials, similar to existing password managers. Each entry can also be tagged, which could later be used by other extensions. -For example, entries tagged "E-Mail" could be created, which could then be used by an email client extension to automatically connect to mail servers. -Any other extension could access specific entries in the password database (or other extensions' data) to easily provide its service. -But of course, each extension can also create its own tables as needed for its specific use case. -HaexHub takes care of secure storage and, if configured, conflict-free synchronization. -Each user can expand their HaexHub with the individual functionality they need. And since all settings for these extensions can be stored in the SQLite database, they can be easily and seamlessly synchronized and used across multiple devices. The user only needs to set up their extensions once on one device and can then use them on all other devices and systems without further action. +HaexHub itself is **cross-platform** and runs on: -Another example of an extension would be file synchronization, which will also be a core extension. -This extension allows users to easily synchronize their files across different devices and systems. It can be configured on each device which files and folders should be synchronized and how. For instance, one might want to upload pictures and videos from their smartphone to an S3 bucket/Google Drive/Dropbox and their desktop PC. However, one probably doesn't want all pictures from the S3 bucket/Google Drive/Dropbox/Desktop to be synchronized back to the smartphone. All these configurations will again be stored in the SQLite database and, where possible, synchronized across all devices and systems. +- 💻 Windows, macOS, Linux +- 📱 Android, iOS +- 🧠 Desktops, laptops, tablets, smartphones -Further examples of extensions include calendars, (collaborative) document management, contacts, messengers, and in the distant future, a browser and payment service (GNU Taler perhaps?!). +All user and extension data is stored in a **locally encrypted SQLite database**. +To sync across devices, HaexHub can connect to a **synchronization server** — which you can even **self-host** for maximum independence. -\*Fundamentally, any bundler (Vite, Webpack, Rollup, etc.) and any frontend framework (Vue, React, Angular, Svelte, plain HTML) should be usable. The crucial part is that it's a JS bundle. However, initially, the focus will primarily be on Vite and Vue to demonstrate the general feasibility first. +> 🛡️ HaexHub is built on the principles of **privacy, security, and digital sovereignty**. -## Preperation +The user is always in control of their data — deciding what to share, and with whom. -install: +--- -- [nodejs/nvm](https://nodejs.org/en/download) -- [tauri](https://v2.tauri.app/start/prerequisites/) -- [rust](https://v2.tauri.app/start/prerequisites/#rust) -- [android-studio](https://developer.android.com/studio?hl=de) -- webkit2gtk + GTK3 +## 🧠 Technical Foundations + +HaexHub is powered by **[Tauri](https://v2.tauri.app/)** — a secure, efficient framework for building native apps from web technologies. + +Unlike Electron (used by apps like VS Code), Tauri: + +- Uses **native rendering engines** (WebView2, WKWebView, WebKitGTK) +- Produces **smaller, faster apps** +- Enforces **strong sandboxing and permission models** + +HaexHub builds upon Tauri’s security features, extending them to third-party extensions. + +### 🏡 Local-first by Design + +HaexHub follows a **strict local-first architecture**: + +- Works **offline** without accounts or internet. +- Stores data locally in **encrypted SQLite**. +- Uses **CRDTs (Conflict-free Replicated Data Types)** for safe synchronization across devices — even with encrypted data. + +Unlike many “local-first” apps, HaexHub doesn’t just cache data in the browser. +Your data truly resides **on your disk**, not under a browser’s limited storage policy. + +Optionally, HaexHub can sync databases via a backend service — self-hosted or external — with optional **end-to-end encryption**. + +--- + +## 🧩 Extensions + +Extensions are the heart of HaexHub. + +Everything the user interacts with — from password management to file syncing — will be implemented as **extensions**. + +There are two types: + +- **Official/Core Extensions** +- **Third-Party Extensions** + +Each extension is a **web app** bundled via your preferred frontend stack: + +> Vue, React, Svelte, Angular, Vite, Webpack, Rollup — you name it. + +### 🔐 Example: Password Manager + +A first official extension will be a **Password Manager**, built with **Vue/Nuxt**: + +- Declares database permissions via its manifest. +- Manages login credentials locally in encrypted SQLite. +- Can tag entries (e.g. “Email”) for use by other extensions — such as an email client. + +### 🗂 Example: File Synchronization + +Another planned core extension will handle **file synchronization**: + +- Syncs files/folders between devices and cloud providers (e.g. S3, Google Drive, Dropbox). +- Lets users define sync rules per device. +- Stores configuration securely in the local database. + +### 💬 Future Extensions + +- Calendar & Contacts +- Collaborative document management +- Messenger +- Browser & Payment Services (e.g., GNU Taler integration) + +With this modular design, HaexHub can evolve into a true **European alternative to WeChat** — but open, federated, and privacy-first. + +--- + +## 🧰 Installation & Setup + +### 📦 Prerequisites + +Install the following dependencies: + +- [Node.js / nvm](https://nodejs.org/en/download) +- [Tauri](https://v2.tauri.app/start/prerequisites/) +- [Rust](https://v2.tauri.app/start/prerequisites/#rust) +- [Android Studio](https://developer.android.com/studio?hl=de) +- WebKit2GTK + GTK3 + +#### 🐧 Debian / Ubuntu ```bash -# debian/ubuntu sudo apt update sudo apt install \ libwebkit2gtk-4.1-dev \ @@ -84,8 +147,9 @@ sudo apt install \ librsvg2-dev ``` +#### 🦊 Fedora + ```bash -# fedora sudo dnf install \ webkit2gtk4.1-devel \ gtk3-devel \ @@ -93,11 +157,24 @@ sudo dnf install \ 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` +#### ⚙️ Development -``` +Make sure port 3003 is available (or adjust it in `nuxt.config.ts` and `src-tauri/tauri.conf.json`). + +```bash git clone https://github.com/haexhub/haex-vault.git cd haex-vault -pnpm i +pnpm install pnpm tauri dev ``` + +#### 🧭 Summary + +HaexHub aims to: + +- Simplify cross-platform app development +- Empower users with local-first privacy +- Enable developers to create modular, permissioned extensions +- Bridge the gap between web and native worlds + +HaexHub is the foundation for a decentralized, privacy-friendly, European “everything app.” diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8c406ee..e51247f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5220,9 +5220,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" -version = "11.0.1" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" +checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424" dependencies = [ "thiserror 2.0.17", "ts-rs-macros", @@ -5230,9 +5230,9 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "11.0.1" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" +checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" dependencies = [ "proc-macro2", "quote", @@ -6424,9 +6424,9 @@ dependencies = [ [[package]] name = "zip" -version = "5.1.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" dependencies = [ "aes", "arbitrary", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 66d357f..7e0ccc8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -49,8 +49,8 @@ tauri-plugin-os = "2.3" tauri-plugin-persisted-scope = "2.3.2" tauri-plugin-store = "2.4.0" thiserror = "2.0.17" -ts-rs = { version = "11.0.1", features = ["serde-compat"] } +ts-rs = { version = "11.1.0", features = ["serde-compat"] } uhlc = "0.8.2" uuid = { version = "1.18.1", features = ["v4"] } -zip = "5.1.1" +zip = "6.0.0" url = "2.5.7" diff --git a/src-tauri/bindings/ExtensionInfoResponse.ts b/src-tauri/bindings/ExtensionInfoResponse.ts index 99a8a94..5f2f215 100644 --- a/src-tauri/bindings/ExtensionInfoResponse.ts +++ b/src-tauri/bindings/ExtensionInfoResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ExtensionInfoResponse = { keyHash: string, name: string, fullId: string, version: string, displayName: string | null, namespace: string | null, allowedOrigin: string, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, }; +export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, devServerUrl: string | null, }; diff --git a/src-tauri/bindings/ExtensionManifest.ts b/src-tauri/bindings/ExtensionManifest.ts index 66d12eb..1ccc8d7 100644 --- a/src-tauri/bindings/ExtensionManifest.ts +++ b/src-tauri/bindings/ExtensionManifest.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ExtensionPermissions } from "./ExtensionPermissions"; -export type ExtensionManifest = { id: string, name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, }; +export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, }; diff --git a/src-tauri/bindings/ExtensionPreview.ts b/src-tauri/bindings/ExtensionPreview.ts index 52a217e..a98130b 100644 --- a/src-tauri/bindings/ExtensionPreview.ts +++ b/src-tauri/bindings/ExtensionPreview.ts @@ -2,4 +2,4 @@ import type { ExtensionManifest } from "./ExtensionManifest"; import type { ExtensionPermissions } from "./ExtensionPermissions"; -export type ExtensionPreview = { manifest: ExtensionManifest, is_valid_signature: boolean, key_hash: string, editable_permissions: ExtensionPermissions, }; +export type ExtensionPreview = { manifest: ExtensionManifest, is_valid_signature: boolean, editable_permissions: ExtensionPermissions, }; diff --git a/src-tauri/database/migrations/0002_amazing_iron_fist.sql b/src-tauri/database/migrations/0002_amazing_iron_fist.sql new file mode 100644 index 0000000..a3d2f35 --- /dev/null +++ b/src-tauri/database/migrations/0002_amazing_iron_fist.sql @@ -0,0 +1,22 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_haex_extensions` ( + `id` text PRIMARY KEY NOT NULL, + `public_key` text NOT NULL, + `name` text NOT NULL, + `version` text NOT NULL, + `author` text, + `description` text, + `entry` text DEFAULT 'index.html' NOT NULL, + `homepage` text, + `enabled` integer DEFAULT true, + `icon` text, + `signature` text NOT NULL, + `haex_tombstone` integer, + `haex_timestamp` text +); +--> statement-breakpoint +INSERT INTO `__new_haex_extensions`("id", "public_key", "name", "version", "author", "description", "entry", "homepage", "enabled", "icon", "signature", "haex_tombstone", "haex_timestamp") SELECT "id", "public_key", "name", "version", "author", "description", "entry", "homepage", "enabled", "icon", "signature", "haex_tombstone", "haex_timestamp" FROM `haex_extensions`;--> statement-breakpoint +DROP TABLE `haex_extensions`;--> statement-breakpoint +ALTER TABLE `__new_haex_extensions` RENAME TO `haex_extensions`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `haex_extensions_public_key_name_unique` ON `haex_extensions` (`public_key`,`name`); \ No newline at end of file diff --git a/src-tauri/database/migrations/meta/0002_snapshot.json b/src-tauri/database/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..b9a251e --- /dev/null +++ b/src-tauri/database/migrations/meta/0002_snapshot.json @@ -0,0 +1,930 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5387568f-75b3-4a85-86c5-67f539c3fedf", + "prevId": "862ac1d5-3065-4244-8652-2b6782254862", + "tables": { + "haex_crdt_configs": { + "name": "haex_crdt_configs", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_crdt_logs": { + "name": "haex_crdt_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "row_pks": { + "name": "row_pks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "op_type": { + "name": "op_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_name": { + "name": "column_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_haex_timestamp": { + "name": "idx_haex_timestamp", + "columns": [ + "haex_timestamp" + ], + "isUnique": false + }, + "idx_table_row": { + "name": "idx_table_row", + "columns": [ + "table_name", + "row_pks" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_crdt_snapshots": { + "name": "haex_crdt_snapshots", + "columns": { + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created": { + "name": "created", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epoch_hlc": { + "name": "epoch_hlc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_url": { + "name": "location_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_size_bytes": { + "name": "file_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_extension_permissions": { + "name": "haex_extension_permissions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "extension_id": { + "name": "extension_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "constraints": { + "name": "constraints", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'denied'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "haex_extension_permissions_extension_id_resource_type_action_target_unique": { + "name": "haex_extension_permissions_extension_id_resource_type_action_target_unique", + "columns": [ + "extension_id", + "resource_type", + "action", + "target" + ], + "isUnique": true + } + }, + "foreignKeys": { + "haex_extension_permissions_extension_id_haex_extensions_id_fk": { + "name": "haex_extension_permissions_extension_id_haex_extensions_id_fk", + "tableFrom": "haex_extension_permissions", + "tableTo": "haex_extensions", + "columnsFrom": [ + "extension_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_extensions": { + "name": "haex_extensions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'index.html'" + }, + "homepage": { + "name": "homepage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "haex_extensions_public_key_name_unique": { + "name": "haex_extensions_public_key_name_unique", + "columns": [ + "public_key", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_notifications": { + "name": "haex_notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_settings": { + "name": "haex_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_group_items": { + "name": "haex_passwords_group_items", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": { + "name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk", + "tableFrom": "haex_passwords_group_items", + "tableTo": "haex_passwords_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": { + "name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk", + "tableFrom": "haex_passwords_group_items", + "tableTo": "haex_passwords_item_details", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "haex_passwords_group_items_item_id_group_id_pk": { + "columns": [ + "item_id", + "group_id" + ], + "name": "haex_passwords_group_items_item_id_group_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_groups": { + "name": "haex_passwords_groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": { + "name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk", + "tableFrom": "haex_passwords_groups", + "tableTo": "haex_passwords_groups", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_item_details": { + "name": "haex_passwords_item_details", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_item_history": { + "name": "haex_passwords_item_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "changed_property": { + "name": "changed_property", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": { + "name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk", + "tableFrom": "haex_passwords_item_history", + "tableTo": "haex_passwords_item_details", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_item_key_values": { + "name": "haex_passwords_item_key_values", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": { + "name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk", + "tableFrom": "haex_passwords_item_key_values", + "tableTo": "haex_passwords_item_details", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src-tauri/database/migrations/meta/_journal.json b/src-tauri/database/migrations/meta/_journal.json index 62224ea..4bd43e1 100644 --- a/src-tauri/database/migrations/meta/_journal.json +++ b/src-tauri/database/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1759418087677, "tag": "0001_green_stark_industries", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1760272083150, + "tag": "0002_amazing_iron_fist", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src-tauri/database/schemas/haex.ts b/src-tauri/database/schemas/haex.ts index 6c6a4f0..4a7f77b 100644 --- a/src-tauri/database/schemas/haex.ts +++ b/src-tauri/database/schemas/haex.ts @@ -23,26 +23,32 @@ export const haexSettings = sqliteTable(tableNames.haex.settings.name, { export type InsertHaexSettings = typeof haexSettings.$inferInsert export type SelectHaexSettings = typeof haexSettings.$inferSelect -export const haexExtensions = sqliteTable(tableNames.haex.extensions.name, { - id: text() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - author: text(), - description: text(), - entry: text(), - homepage: text(), - enabled: integer({ mode: 'boolean' }), - icon: text(), - name: text(), - public_key: text(), - signature: text(), - url: text(), - version: text(), - haexTombstone: integer(tableNames.haex.extensions.columns.haexTombstone, { - mode: 'boolean', - }), - haexTimestamp: text(tableNames.haex.extensions.columns.haexTimestamp), -}) +export const haexExtensions = sqliteTable( + tableNames.haex.extensions.name, + { + id: text() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + public_key: text().notNull(), + name: text().notNull(), + version: text().notNull(), + author: text(), + description: text(), + entry: text().notNull().default('index.html'), + homepage: text(), + enabled: integer({ mode: 'boolean' }).default(true), + icon: text(), + signature: text().notNull(), + haexTombstone: integer(tableNames.haex.extensions.columns.haexTombstone, { + mode: 'boolean', + }), + haexTimestamp: text(tableNames.haex.extensions.columns.haexTimestamp), + }, + (table) => [ + // UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren + unique().on(table.public_key, table.name), + ], +) export type InsertHaexExtensions = typeof haexExtensions.$inferInsert export type SelectHaexExtensions = typeof haexExtensions.$inferSelect diff --git a/src-tauri/database/vault.db b/src-tauri/database/vault.db index 9d40fc30a2b2a8e172b16d316b59032ddbe06455..2982f873511ceb875e18f0f58909cb7d38177c3d 100644 GIT binary patch delta 637 zcmZo@;Al9&F+p08g@J)V35W%Nm=TCYC+Zk8vTRIP!q3IbFUi2anLmwRah-E)R6|WM12(Q@0g*GzH`f}{z=FOTQ(wwoiuq3fr7Rc7t2C^r= zV^x+{bPjTL3~^Nmadh%=RZz-EOs$Adttd&&E6&W%D^{9(ky(wapfo8bGdVsxwQ_PH zi!gIuV(#Pu77_Nc)FO}~kl5rfHUl{Y7gsmOP@fP5^~}7K)C#?flH4411wa1~1;0=q zpUG`(YU*$$B}JvFI$WFynaTNi2?`}Z8$rC{%=Em(lF}le@!FI7S*5nyOk~{1Hd%+! zXS?GJ#{bMhe8~(vvF3~nAPa2e*~A02r!&rBl%0H*i*tJaJVqf#rR|Ax8JQT_fKmDp zO;Q*b=ZtZaU$Cw-ZPZj?7mw6tY>WoG6wTA|2>-?d{hAtInwMEvnmYZ!JVx=!f7xXO gi_uj~_T!M2C{9q&NI+;!&`AJkPS9Lr-~fq30Dfk>HUIzs delta 189 zcmZoTz|qjaF+p08nSp^p0f=EhaH5VeBlE_DCHx#r{AmpQoB7i=3o1nMPfnAc2Ne0v z!2gH;`({Cd*ZiBm>vL{c6rg~Nwly#^3aGQcWe{MrVc=WDm&`kdH<8hX`!|<2=Qj=q z_OEPa?9*86Sno5HFhATZC?LYPU1}2JMm8262L_JG8jJzk#b+}9XJ%nwU{u`Pz%0wS ZJ#j806XRqjc8ASRSz{R&88`r~0{{?uGYJ3y diff --git a/src-tauri/gen/android/app/src/main/assets/database/vault.db b/src-tauri/gen/android/app/src/main/assets/database/vault.db index 9d40fc30a2b2a8e172b16d316b59032ddbe06455..2982f873511ceb875e18f0f58909cb7d38177c3d 100644 GIT binary patch delta 637 zcmZo@;Al9&F+p08g@J)V35W%Nm=TCYC+Zk8vTRIP!q3IbFUi2anLmwRah-E)R6|WM12(Q@0g*GzH`f}{z=FOTQ(wwoiuq3fr7Rc7t2C^r= zV^x+{bPjTL3~^Nmadh%=RZz-EOs$Adttd&&E6&W%D^{9(ky(wapfo8bGdVsxwQ_PH zi!gIuV(#Pu77_Nc)FO}~kl5rfHUl{Y7gsmOP@fP5^~}7K)C#?flH4411wa1~1;0=q zpUG`(YU*$$B}JvFI$WFynaTNi2?`}Z8$rC{%=Em(lF}le@!FI7S*5nyOk~{1Hd%+! zXS?GJ#{bMhe8~(vvF3~nAPa2e*~A02r!&rBl%0H*i*tJaJVqf#rR|Ax8JQT_fKmDp zO;Q*b=ZtZaU$Cw-ZPZj?7mw6tY>WoG6wTA|2>-?d{hAtInwMEvnmYZ!JVx=!f7xXO gi_uj~_T!M2C{9q&NI+;!&`AJkPS9Lr-~fq30Dfk>HUIzs delta 189 zcmZoTz|qjaF+p08nSp^p0f=EhaH5VeBlE_DCHx#r{AmpQoB7i=3o1nMPfnAc2Ne0v z!2gH;`({Cd*ZiBm>vL{c6rg~Nwly#^3aGQcWe{MrVc=WDm&`kdH<8hX`!|<2=Qj=q z_OEPa?9*86Sno5HFhATZC?LYPU1}2JMm8262L_JG8jJzk#b+}9XJ%nwU{u`Pz%0wS ZJ#j806XRqjc8ASRSz{R&88`r~0{{?uGYJ3y diff --git a/src-tauri/src/database/generated.rs b/src-tauri/src/database/generated.rs index fd852aa..63befd0 100644 --- a/src-tauri/src/database/generated.rs +++ b/src-tauri/src/database/generated.rs @@ -38,28 +38,21 @@ impl HaexSettings { #[serde(rename_all = "camelCase")] pub struct HaexExtensions { pub id: String, + pub public_key: String, + pub name: String, + pub version: String, #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub entry: Option, + pub entry: String, #[serde(skip_serializing_if = "Option::is_none")] pub homepage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub public_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub signature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, + pub signature: String, #[serde(skip_serializing_if = "Option::is_none")] pub haex_tombstone: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -70,19 +63,18 @@ impl HaexExtensions { pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { Ok(Self { id: row.get(0)?, - author: row.get(1)?, - description: row.get(2)?, - entry: row.get(3)?, - homepage: row.get(4)?, - enabled: row.get(5)?, - icon: row.get(6)?, - name: row.get(7)?, - public_key: row.get(8)?, - signature: row.get(9)?, - url: row.get(10)?, - version: row.get(11)?, - haex_tombstone: row.get(12)?, - haex_timestamp: row.get(13)?, + public_key: row.get(1)?, + name: row.get(2)?, + version: row.get(3)?, + author: row.get(4)?, + description: row.get(5)?, + entry: row.get(6)?, + homepage: row.get(7)?, + enabled: row.get(8)?, + icon: row.get(9)?, + signature: row.get(10)?, + haex_tombstone: row.get(11)?, + haex_timestamp: row.get(12)?, }) } } diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 8766394..d657b4e 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -28,13 +28,14 @@ pub struct CachedPermission { #[derive(Debug, Clone)] pub struct MissingExtension { - pub full_extension_id: String, + pub id: String, + pub public_key: String, pub name: String, pub version: String, } struct ExtensionDataFromDb { - full_extension_id: String, + id: String, manifest: ExtensionManifest, enabled: bool, } @@ -153,55 +154,19 @@ impl ExtensionManager { pub fn get_extension_dir( &self, app_handle: &AppHandle, - key_hash: &str, + public_key: &str, extension_name: &str, extension_version: &str, ) -> Result { let specific_extension_dir = self .get_base_extension_dir(app_handle)? - .join(key_hash) + .join(public_key) .join(extension_name) .join(extension_version); Ok(specific_extension_dir) } - pub fn get_extension_path_by_full_extension_id( - &self, - app_handle: &AppHandle, - full_extension_id: &str, - ) -> Result { - // Parse full_extension_id: key_hash_name_version - // Split on first underscore to get key_hash - let first_underscore = - full_extension_id - .find('_') - .ok_or_else(|| ExtensionError::ValidationError { - reason: format!("Invalid full_extension_id format: {}", full_extension_id), - })?; - - let key_hash = &full_extension_id[..first_underscore]; - let rest = &full_extension_id[first_underscore + 1..]; - - // Split on last underscore to get version - let last_underscore = rest - .rfind('_') - .ok_or_else(|| ExtensionError::ValidationError { - reason: format!("Invalid full_extension_id format: {}", full_extension_id), - })?; - - let name = &rest[..last_underscore]; - let version = &rest[last_underscore + 1..]; - - // Build hierarchical path: key_hash/name/version/ - let specific_extension_dir = self - .get_base_extension_dir(app_handle)? - .join(key_hash) - .join(name) - .join(version); - - Ok(specific_extension_dir) - } pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> { if extension.id.is_empty() { @@ -251,63 +216,106 @@ impl ExtensionManager { 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(()); + /// Find extension ID by public_key and name (checks dev extensions first, then production) + fn find_extension_id_by_public_key_and_name( + &self, + public_key: &str, + name: &str, + ) -> Result, ExtensionError> { + // 1. Check dev extensions first (higher priority) + let dev_extensions = self.dev_extensions.lock().map_err(|e| { + ExtensionError::MutexPoisoned { + reason: e.to_string(), + } + })?; + + for (id, ext) in dev_extensions.iter() { + if ext.manifest.public_key == public_key && ext.manifest.name == name { + return Ok(Some((id.clone(), ext.clone()))); } } - { - let mut prod_extensions = self.production_extensions.lock().unwrap(); - if prod_extensions.remove(extension_id).is_some() { - return Ok(()); + // 2. Check production extensions + let prod_extensions = self.production_extensions.lock().map_err(|e| { + ExtensionError::MutexPoisoned { + reason: e.to_string(), + } + })?; + + for (id, ext) in prod_extensions.iter() { + if ext.manifest.public_key == public_key && ext.manifest.name == name { + return Ok(Some((id.clone(), ext.clone()))); } } - Err(ExtensionError::NotFound { - id: extension_id.to_string(), - }) + Ok(None) } - pub async fn remove_extension_by_full_id( + /// Get extension by public_key and name (used by frontend) + pub fn get_extension_by_public_key_and_name( &self, - app_handle: &AppHandle, - full_extension_id: &str, - state: &State<'_, AppState>, - ) -> Result<(), ExtensionError> { - // Parse full_extension_id: key_hash_name_version - // Since _ is not allowed in name and version, we can split safely - let parts: Vec<&str> = full_extension_id.split('_').collect(); + public_key: &str, + name: &str, + ) -> Result, ExtensionError> { + Ok(self + .find_extension_id_by_public_key_and_name(public_key, name)? + .map(|(_, ext)| ext)) + } - if parts.len() != 3 { - return Err(ExtensionError::ValidationError { - reason: format!( - "Invalid full_extension_id format (expected 3 parts): {}", - full_extension_id - ), - }); + pub fn remove_extension( + &self, + public_key: &str, + name: &str, + ) -> Result<(), ExtensionError> { + let (id, _) = self + .find_extension_id_by_public_key_and_name(public_key, name)? + .ok_or_else(|| ExtensionError::NotFound { + public_key: public_key.to_string(), + name: name.to_string(), + })?; + + // Remove from dev extensions first + { + let mut dev_extensions = self.dev_extensions.lock().map_err(|e| { + ExtensionError::MutexPoisoned { + reason: e.to_string(), + } + })?; + if dev_extensions.remove(&id).is_some() { + return Ok(()); + } } - let key_hash = parts[0]; - let name = parts[1]; - let version = parts[2]; + // Remove from production extensions + { + let mut prod_extensions = self.production_extensions.lock().map_err(|e| { + ExtensionError::MutexPoisoned { + reason: e.to_string(), + } + })?; + prod_extensions.remove(&id); + } - self.remove_extension_internal(app_handle, key_hash, name, version, state) - .await + Ok(()) } pub async fn remove_extension_internal( &self, app_handle: &AppHandle, - key_hash: &str, + public_key: &str, extension_name: &str, extension_version: &str, state: &State<'_, AppState>, ) -> Result<(), ExtensionError> { - // Erstelle full_extension_id: key_hash_name_version - let full_extension_id = format!("{}_{}_{}",key_hash, extension_name, extension_version); + // Get the extension from memory to get its ID + let extension = self + .get_extension_by_public_key_and_name(public_key, extension_name)? + .ok_or_else(|| ExtensionError::NotFound { + public_key: public_key.to_string(), + name: extension_name.to_string(), + })?; + + // Lösche Permissions und Extension-Eintrag in einer Transaktion with_connection(&state.db, |conn| { @@ -317,31 +325,31 @@ impl ExtensionManager { reason: "Failed to lock HLC service".to_string(), })?; - // Lösche alle Permissions mit full_extension_id + // Lösche alle Permissions mit extension_id PermissionManager::delete_permissions_in_transaction( &tx, &hlc_service, - &full_extension_id, + &extension.id, )?; - // Lösche Extension-Eintrag mit full_extension_id + // Lösche Extension-Eintrag mit extension_id let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS); SqlExecutor::execute_internal_typed( &tx, &hlc_service, &sql, - rusqlite::params![full_extension_id], + rusqlite::params![&extension.id], )?; tx.commit().map_err(DatabaseError::from) })?; - // Entferne aus dem In-Memory-Manager mit full_extension_id - self.remove_extension(&full_extension_id)?; + // Entferne aus dem In-Memory-Manager + self.remove_extension(public_key, extension_name)?; - // Lösche nur den spezifischen Versions-Ordner: key_hash/name/version + // Lösche nur den spezifischen Versions-Ordner: public_key/name/version let extension_dir = - self.get_extension_dir(app_handle, key_hash, extension_name, extension_version)?; + self.get_extension_dir(app_handle, public_key, extension_name, extension_version)?; if extension_dir.exists() { std::fs::remove_dir_all(&extension_dir).map_err(|e| { @@ -388,13 +396,11 @@ impl ExtensionManager { ) .is_ok(); - let key_hash = extracted.manifest.calculate_key_hash()?; let editable_permissions = extracted.manifest.to_editable_permissions(); Ok(ExtensionPreview { manifest: extracted.manifest.clone(), is_valid_signature, - key_hash, editable_permissions, }) } @@ -416,11 +422,9 @@ impl ExtensionManager { ) .map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?; - let full_extension_id = extracted.manifest.full_extension_id()?; - let extensions_dir = self.get_extension_dir( &app_handle, - &extracted.manifest.calculate_key_hash()?, + &extracted.manifest.public_key, &extracted.manifest.name, &extracted.manifest.version, )?; @@ -451,7 +455,9 @@ impl ExtensionManager { } } - let permissions = custom_permissions.to_internal_permissions(&full_extension_id); + // Generate UUID for extension (Drizzle's $defaultFn only works from JS, not raw SQL) + let extension_id = uuid::Uuid::new_v4().to_string(); + let permissions = custom_permissions.to_internal_permissions(&extension_id); // Extension-Eintrag und Permissions in einer Transaktion speichern with_connection(&state.db, |conn| { @@ -461,9 +467,9 @@ impl ExtensionManager { reason: "Failed to lock HLC service".to_string(), })?; - // 1. Extension-Eintrag erstellen (oder aktualisieren falls schon vorhanden) + // 1. Extension-Eintrag erstellen mit generierter UUID let insert_ext_sql = format!( - "INSERT OR REPLACE INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", TABLE_EXTENSIONS ); @@ -472,7 +478,7 @@ impl ExtensionManager { &hlc_service, &insert_ext_sql, rusqlite::params![ - full_extension_id, + extension_id, extracted.manifest.name, extracted.manifest.version, extracted.manifest.author, @@ -512,12 +518,12 @@ impl ExtensionManager { )?; } - tx.commit().map_err(DatabaseError::from) + tx.commit().map_err(DatabaseError::from)?; + Ok(extension_id.clone()) })?; let extension = Extension { - id: full_extension_id.clone(), - name: extracted.manifest.name.clone(), + id: extension_id.clone(), source: ExtensionSource::Production { path: extensions_dir.clone(), version: extracted.manifest.version.clone(), @@ -529,7 +535,7 @@ impl ExtensionManager { self.add_production_extension(extension)?; - Ok(full_extension_id) + Ok(extension_id) } /// Scannt das Dateisystem beim Start und lädt alle installierten Erweiterungen. @@ -569,7 +575,7 @@ impl ExtensionManager { let mut data = Vec::new(); for result in results { - let full_extension_id = result["id"] + let id = result["id"] .as_str() .ok_or_else(|| DatabaseError::SerializationError { reason: "Missing id field".to_string(), @@ -577,12 +583,6 @@ impl ExtensionManager { .to_string(); let manifest = ExtensionManifest { - id: result["name"] - .as_str() - .ok_or_else(|| DatabaseError::SerializationError { - reason: "Missing name field".to_string(), - })? - .to_string(), name: result["name"] .as_str() .ok_or_else(|| DatabaseError::SerializationError { @@ -611,7 +611,7 @@ impl ExtensionManager { .unwrap_or(false); data.push(ExtensionDataFromDb { - full_extension_id, + id, manifest, enabled, }); @@ -625,15 +625,21 @@ impl ExtensionManager { eprintln!("DEBUG: Found {} extensions in database", extensions.len()); for extension_data in extensions { - let full_extension_id = extension_data.full_extension_id; - eprintln!("DEBUG: Processing extension: {}", full_extension_id); - let extension_path = - self.get_extension_path_by_full_extension_id(app_handle, &full_extension_id)?; + let extension_id = extension_data.id; + eprintln!("DEBUG: Processing extension: {}", extension_id); + + // Use public_key/name/version path structure + let extension_path = self.get_extension_dir( + app_handle, + &extension_data.manifest.public_key, + &extension_data.manifest.name, + &extension_data.manifest.version, + )?; if !extension_path.exists() || !extension_path.join("manifest.json").exists() { eprintln!( "DEBUG: Extension files missing for: {} at {:?}", - full_extension_id, extension_path + extension_id, extension_path ); self.missing_extensions .lock() @@ -641,7 +647,8 @@ impl ExtensionManager { reason: e.to_string(), })? .push(MissingExtension { - full_extension_id: full_extension_id.clone(), + id: extension_id.clone(), + public_key: extension_data.manifest.public_key.clone(), name: extension_data.manifest.name.clone(), version: extension_data.manifest.version.clone(), }); @@ -650,12 +657,11 @@ impl ExtensionManager { eprintln!( "DEBUG: Extension loaded successfully: {}", - full_extension_id + extension_id ); let extension = Extension { - id: full_extension_id.clone(), - name: extension_data.manifest.name.clone(), + id: extension_id.clone(), source: ExtensionSource::Production { path: extension_path, version: extension_data.manifest.version.clone(), @@ -665,7 +671,7 @@ impl ExtensionManager { last_accessed: SystemTime::now(), }; - loaded_extension_ids.push(full_extension_id.clone()); + loaded_extension_ids.push(extension_id.clone()); self.add_production_extension(extension)?; } diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs index 9775cf0..1db5d03 100644 --- a/src-tauri/src/extension/core/manifest.rs +++ b/src-tauri/src/extension/core/manifest.rs @@ -1,4 +1,3 @@ -use crate::extension::crypto::ExtensionCrypto; use crate::extension::error::ExtensionError; use crate::extension::permissions::types::{ Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints, @@ -33,7 +32,6 @@ pub struct PermissionEntry { pub struct ExtensionPreview { pub manifest: ExtensionManifest, pub is_valid_signature: bool, - pub key_hash: String, pub editable_permissions: EditablePermissions, } /// Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI. @@ -56,7 +54,6 @@ pub type EditablePermissions = ExtensionPermissions; #[derive(Serialize, Deserialize, Clone, Debug, TS)] #[ts(export)] pub struct ExtensionManifest { - pub id: String, pub name: String, pub version: String, pub author: Option, @@ -70,28 +67,6 @@ pub struct ExtensionManifest { } impl ExtensionManifest { - pub fn calculate_key_hash(&self) -> Result { - ExtensionCrypto::calculate_key_hash(&self.public_key) - .map_err(|e| ExtensionError::InvalidPublicKey { reason: e }) - } - - pub fn full_extension_id(&self) -> Result { - // Validate that name and version don't contain underscores - if self.name.contains('_') { - return Err(ExtensionError::ValidationError { - reason: format!("Extension name cannot contain underscores: {}", self.name), - }); - } - if self.version.contains('_') { - return Err(ExtensionError::ValidationError { - reason: format!("Extension version cannot contain underscores: {}", self.version), - }); - } - - let key_hash = self.calculate_key_hash()?; - Ok(format!("{}_{}_{}", key_hash, self.name, self.version)) - } - /// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell, /// indem der Standardstatus `Granted` gesetzt wird. pub fn to_editable_permissions(&self) -> EditablePermissions { @@ -189,43 +164,41 @@ impl ExtensionPermissions { #[ts(export)] #[serde(rename_all = "camelCase")] pub struct ExtensionInfoResponse { - pub key_hash: String, + pub id: String, + pub public_key: String, pub name: String, - pub full_id: String, pub version: String, - pub display_name: Option, - pub namespace: Option, - pub allowed_origin: String, + pub author: Option, pub enabled: bool, pub description: Option, pub homepage: Option, pub icon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dev_server_url: Option, } impl ExtensionInfoResponse { pub fn from_extension( extension: &crate::extension::core::types::Extension, ) -> Result { - use crate::extension::core::types::get_tauri_origin; + use crate::extension::core::types::ExtensionSource; - // Always use the current Tauri origin to support all platforms (Desktop, Android, iOS) - let allowed_origin = get_tauri_origin(); - - let key_hash = extension.manifest.calculate_key_hash()?; - let full_id = extension.manifest.full_extension_id()?; + let dev_server_url = match &extension.source { + ExtensionSource::Development { dev_server_url, .. } => Some(dev_server_url.clone()), + ExtensionSource::Production { .. } => None, + }; Ok(Self { - key_hash, + id: extension.id.clone(), + public_key: extension.manifest.public_key.clone(), name: extension.manifest.name.clone(), - full_id, version: extension.manifest.version.clone(), - display_name: Some(extension.manifest.name.clone()), - namespace: extension.manifest.author.clone(), - allowed_origin, + author: extension.manifest.author.clone(), enabled: extension.enabled, description: extension.manifest.description.clone(), homepage: extension.manifest.homepage.clone(), icon: extension.manifest.icon.clone(), + dev_server_url, }) } } diff --git a/src-tauri/src/extension/core/protocol.rs b/src-tauri/src/extension/core/protocol.rs index 70eb56e..47a432e 100644 --- a/src-tauri/src/extension/core/protocol.rs +++ b/src-tauri/src/extension/core/protocol.rs @@ -3,6 +3,7 @@ use crate::extension::core::types::get_tauri_origin; use crate::extension::error::ExtensionError; use crate::AppState; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use mime; use serde::Deserialize; use serde::Serialize; @@ -23,8 +24,9 @@ lazy_static::lazy_static! { } #[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] struct ExtensionInfo { - key_hash: String, + public_key: String, name: String, version: String, } @@ -88,7 +90,7 @@ impl From for DataProcessingError { pub fn resolve_secure_extension_asset_path( app_handle: &AppHandle, state: &State, - key_hash: &str, + public_key: &str, extension_name: &str, extension_version: &str, requested_asset_path: &str, @@ -115,7 +117,7 @@ pub fn resolve_secure_extension_asset_path( let specific_extension_dir = state.extension_manager.get_extension_dir( app_handle, - key_hash, + public_key, extension_name, extension_version, )?; @@ -218,36 +220,130 @@ pub fn extension_protocol_handler( println!("Origin: {}", origin); println!("Referer: {}", referer); - let info = - match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(&origin, uri_ref, &referer) - { - Ok(decoded) => { - println!("=== Extension Protocol Handler ==="); - println!("Full URI: {}", uri_ref); - println!( - "Encoded Info (aus Origin/URI/Referer/Cache): {}", - encode_hex_for_log(&decoded) - ); // Hilfs-Log - println!("Decoded info:"); - println!(" KeyHash: {}", decoded.key_hash); - println!(" Name: {}", decoded.name); - println!(" Version: {}", decoded.version); - decoded + let path_str = uri_ref.path(); + + // Try to decode base64-encoded extension info from URI + // Format: + // - Desktop: haex-extension:///{assetPath} + // - Android: http://localhost/{base64}/{assetPath} + let host = uri_ref.host().unwrap_or(""); + println!("URI Host: {}", host); + + let (info, segments_after_version) = if host == "localhost" || host == format!("{}.localhost", EXTENSION_PROTOCOL_NAME).as_str() { + // Android format: http://haex-extension.localhost/{base64}/{assetPath} + // Extract base64 from first path segment + println!("Android format detected: http://{}/...", host); + let mut segments_iter = path_str.split('/').filter(|s| !s.is_empty()); + + if let Some(first_segment) = segments_iter.next() { + println!("First path segment (base64): {}", first_segment); + match BASE64_STANDARD.decode(first_segment) { + Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) { + Ok(json_str) => match serde_json::from_str::(&json_str) { + Ok(info) => { + println!("=== Extension Info from path (Android) ==="); + println!(" PublicKey: {}", info.public_key); + println!(" Name: {}", info.name); + println!(" Version: {}", info.version); + cache_extension_info(&info); + + // Remaining segments after base64 are the asset path + let remaining: Vec = segments_iter.map(|s| s.to_string()).collect(); + (info, remaining) + } + Err(e) => { + eprintln!("Failed to parse JSON from base64 path: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Invalid extension info in base64 path: {}", e))) + .map_err(|e| e.into()); + } + }, + Err(e) => { + eprintln!("Failed to decode UTF-8 from base64 path: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Invalid UTF-8 in base64 path: {}", e))) + .map_err(|e| e.into()); + } + }, + Err(e) => { + eprintln!("Failed to decode base64 from path: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Invalid base64 in path: {}", e))) + .map_err(|e| e.into()); + } } + } else { + eprintln!("No path segment found for Android format"); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from("No base64 segment found in path")) + .map_err(|e| e.into()); + } + } else if host != "localhost" && !host.is_empty() { + // Desktop format: haex-extension:///{assetPath} + println!("Desktop format detected: haex-extension:///..."); + match BASE64_STANDARD.decode(host) { + Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) { + Ok(json_str) => match serde_json::from_str::(&json_str) { + Ok(info) => { + println!("=== Extension Info from base64-encoded host ==="); + println!(" PublicKey: {}", info.public_key); + println!(" Name: {}", info.name); + println!(" Version: {}", info.version); + cache_extension_info(&info); + + // Parse path segments as asset path + // Format: haex-extension:///{asset_path} + // All extension info is in the base64-encoded host + let segments: Vec = path_str + .split('/') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + (info, segments) + } + Err(e) => { + eprintln!("Failed to parse JSON from base64 host: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Invalid extension info in base64 host: {}", e))) + .map_err(|e| e.into()); + } + }, + Err(e) => { + eprintln!("Failed to decode UTF-8 from base64 host: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Invalid UTF-8 in base64 host: {}", e))) + .map_err(|e| e.into()); + } + }, Err(e) => { - eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e); + eprintln!("Failed to decode base64 host: {}", e); return Response::builder() .status(400) .header("Access-Control-Allow-Origin", allowed_origin) - .body(Vec::from(format!("Ungültige Anfrage: {}", e))) + .body(Vec::from(format!("Invalid base64 in host: {}", e))) .map_err(|e| e.into()); } - }; + } + } else { + // No base64 host - use path-based parsing (for localhost/Android/Windows) + parse_extension_info_from_path(path_str, origin, uri_ref, referer, &allowed_origin)? + }; - let path_str = uri_ref.path(); - let segments_iter = path_str.split('/').filter(|s| !s.is_empty()); - let resource_segments: Vec<&str> = segments_iter.collect(); - let raw_asset_path = resource_segments.join("/"); + // Construct asset path from remaining segments + let raw_asset_path = segments_after_version.join("/"); // Simple asset loading: if path is empty, serve index.html, otherwise try to load the asset // This is framework-agnostic and lets the file system determine if it exists @@ -263,7 +359,7 @@ pub fn extension_protocol_handler( let absolute_secure_path = resolve_secure_extension_asset_path( app_handle, &state, - &info.key_hash, + &info.public_key, &info.name, &info.version, &asset_to_load, @@ -335,7 +431,7 @@ pub fn extension_protocol_handler( let index_path = resolve_secure_extension_asset_path( app_handle, &state, - &info.key_hash, + &info.public_key, &info.name, &info.version, "index.html", @@ -431,8 +527,8 @@ fn parse_encoded_info_from_origin_or_uri_or_referer_or_cache( println!("Fallback zu Cache"); if let Some(cached_info) = get_cached_extension_info() { println!( - "Gecached Info verwendet: KeyHash={}, Name={}, Version={}", - cached_info.key_hash, cached_info.name, cached_info.version + "Gecached Info verwendet: PublicKey={}, Name={}, Version={}", + cached_info.public_key, cached_info.name, cached_info.version ); return Ok(cached_info); } @@ -517,3 +613,67 @@ fn encode_hex_for_log(info: &ExtensionInfo) -> String { let json_str = serde_json::to_string(info).unwrap_or_default(); hex::encode(json_str.as_bytes()) } + +// Helper function to parse extension info from path segments +fn parse_extension_info_from_path( + path_str: &str, + origin: &str, + uri_ref: &Uri, + referer: &str, + allowed_origin: &str, +) -> Result<(ExtensionInfo, Vec), Box> { + let mut segments_iter = path_str.split('/').filter(|s| !s.is_empty()); + + match (segments_iter.next(), segments_iter.next(), segments_iter.next()) { + (Some(public_key), Some(name), Some(version)) => { + println!("=== Extension Protocol Handler (path-based) ==="); + println!("Full URI: {}", uri_ref); + println!("Parsed from path segments:"); + println!(" PublicKey: {}", public_key); + println!(" Name: {}", name); + println!(" Version: {}", version); + + let info = ExtensionInfo { + public_key: public_key.to_string(), + name: name.to_string(), + version: version.to_string(), + }; + + cache_extension_info(&info); + + // Collect remaining segments as asset path (owned strings) + let remaining: Vec = segments_iter.map(|s| s.to_string()).collect(); + + Ok((info, remaining)) + } + _ => { + // Fallback: Try hex-encoded format for backwards compatibility + match parse_encoded_info_from_origin_or_uri_or_referer_or_cache( + origin, uri_ref, referer, + ) { + Ok(decoded) => { + println!("=== Extension Protocol Handler (legacy hex format) ==="); + println!("Full URI: {}", uri_ref); + println!("Decoded info:"); + println!(" PublicKey: {}", decoded.public_key); + println!(" Name: {}", decoded.name); + println!(" Version: {}", decoded.version); + + // For legacy format, collect all segments after parsing (owned strings) + let segments: Vec = path_str + .split('/') + .filter(|s| !s.is_empty()) + .skip(1) // Skip the hex segment + .map(|s| s.to_string()) + .collect(); + + Ok((decoded, segments)) + } + Err(e) => { + eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e); + Err(format!("Ungültige Anfrage: {}", e).into()) + } + } + } + } +} diff --git a/src-tauri/src/extension/core/types.rs b/src-tauri/src/extension/core/types.rs index 8d851be..340ca02 100644 --- a/src-tauri/src/extension/core/types.rs +++ b/src-tauri/src/extension/core/types.rs @@ -21,11 +21,15 @@ pub enum ExtensionSource { /// Complete extension data structure #[derive(Debug, Clone)] pub struct Extension { + /// UUID from database (primary key) pub id: String, - pub name: String, + /// Extension source (production path or dev server) pub source: ExtensionSource, + /// Extension manifest containing all metadata (name, version, public_key, etc.) pub manifest: ExtensionManifest, + /// Whether the extension is enabled pub enabled: bool, + /// Last time the extension was accessed pub last_accessed: SystemTime, } diff --git a/src-tauri/src/extension/database/mod.rs b/src-tauri/src/extension/database/mod.rs index d58dca0..71b2ef8 100644 --- a/src-tauri/src/extension/database/mod.rs +++ b/src-tauri/src/extension/database/mod.rs @@ -107,11 +107,21 @@ impl<'a> StatementExecutor<'a> { pub async fn extension_sql_execute( sql: &str, params: Vec, - extension_id: String, + public_key: String, + name: String, state: State<'_, AppState>, ) -> Result, ExtensionError> { + // Get extension to retrieve its ID + let extension = state + .extension_manager + .get_extension_by_public_key_and_name(&public_key, &name)? + .ok_or_else(|| ExtensionError::NotFound { + public_key: public_key.clone(), + name: name.clone(), + })?; + // Permission check - SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?; + SqlPermissionValidator::validate_sql(&state, &extension.id, sql).await?; // Parameter validation validate_params(sql, ¶ms)?; @@ -179,11 +189,21 @@ pub async fn extension_sql_execute( pub async fn extension_sql_select( sql: &str, params: Vec, - extension_id: String, + public_key: String, + name: String, state: State<'_, AppState>, ) -> Result, ExtensionError> { + // Get extension to retrieve its ID + let extension = state + .extension_manager + .get_extension_by_public_key_and_name(&public_key, &name)? + .ok_or_else(|| ExtensionError::NotFound { + public_key: public_key.clone(), + name: name.clone(), + })?; + // Permission check - SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?; + SqlPermissionValidator::validate_sql(&state, &extension.id, sql).await?; // Parameter validation validate_params(sql, ¶ms)?; diff --git a/src-tauri/src/extension/error.rs b/src-tauri/src/extension/error.rs index bed0746..d54088e 100644 --- a/src-tauri/src/extension/error.rs +++ b/src-tauri/src/extension/error.rs @@ -39,8 +39,8 @@ pub enum ExtensionError { #[error("Security violation: {reason}")] SecurityViolation { reason: String }, - #[error("Extension not found: {id}")] - NotFound { id: String }, + #[error("Extension not found: {name} (public_key: {public_key})")] + NotFound { public_key: String, name: String }, #[error("Permission denied: {extension_id} cannot {operation} on {resource}")] PermissionDenied { diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 5849fc4..6e49db0 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -16,15 +16,19 @@ pub mod permissions; #[tauri::command] pub fn get_extension_info( - extension_id: String, + public_key: String, + name: String, state: State, -) -> Result { +) -> Result { let extension = state .extension_manager - .get_extension(&extension_id) - .ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?; + .get_extension_by_public_key_and_name(&public_key, &name)? + .ok_or_else(|| ExtensionError::NotFound { + public_key: public_key.clone(), + name: name.clone(), + })?; - ExtensionInfoResponse::from_extension(&extension).map_err(|e| format!("{:?}", e)) + ExtensionInfoResponse::from_extension(&extension) } #[tauri::command] @@ -182,44 +186,247 @@ pub async fn install_extension( #[tauri::command] pub async fn remove_extension( app_handle: AppHandle, - key_hash: &str, - extension_id: &str, - extension_version: &str, + public_key: String, + name: String, + version: String, state: State<'_, AppState>, ) -> Result<(), ExtensionError> { state .extension_manager .remove_extension_internal( &app_handle, - key_hash, - extension_id, - extension_version, + &public_key, + &name, + &version, &state, ) .await } -#[tauri::command] -pub async fn remove_extension_by_full_id( - app_handle: AppHandle, - full_extension_id: String, - state: State<'_, AppState>, -) -> Result<(), ExtensionError> { - state - .extension_manager - .remove_extension_by_full_id(&app_handle, &full_extension_id, &state) - .await -} - #[tauri::command] pub fn is_extension_installed( - extension_id: String, + public_key: String, + name: String, extension_version: String, state: State<'_, AppState>, -) -> Result { - if let Some(ext) = state.extension_manager.get_extension(&extension_id) { +) -> Result { + if let Some(ext) = state + .extension_manager + .get_extension_by_public_key_and_name(&public_key, &name)? + { Ok(ext.manifest.version == extension_version) } else { Ok(false) } } + +#[derive(serde::Deserialize, Debug)] +struct HaextensionConfig { + dev: DevConfig, +} + +#[derive(serde::Deserialize, Debug)] +struct DevConfig { + #[serde(default = "default_port")] + port: u16, + #[serde(default = "default_host")] + host: String, +} + +fn default_port() -> u16 { + 5173 +} + +fn default_host() -> String { + "localhost".to_string() +} + +/// Check if a dev server is reachable by making a simple HTTP request +async fn check_dev_server_health(url: &str) -> bool { + use tauri_plugin_http::reqwest; + use std::time::Duration; + + // Try to connect with a short timeout + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(3)) + .build(); + + if let Ok(client) = client { + // Just check if the root responds (most dev servers respond to / with their app) + if let Ok(response) = client.get(url).send().await { + // Accept any response (200, 404, etc.) - we just want to know the server is running + return response.status().as_u16() < 500; + } + } + + false +} + +#[tauri::command] +pub async fn load_dev_extension( + extension_path: String, + state: State<'_, AppState>, +) -> Result { + use crate::extension::core::{ + manifest::ExtensionManifest, + types::{Extension, ExtensionSource}, + }; + use std::path::PathBuf; + use std::time::SystemTime; + + let extension_path_buf = PathBuf::from(&extension_path); + + // 1. Read haextension.json to get dev server config + let config_path = extension_path_buf.join("haextension.json"); + let (host, port) = if config_path.exists() { + let config_content = std::fs::read_to_string(&config_path).map_err(|e| { + ExtensionError::ValidationError { + reason: format!("Failed to read haextension.json: {}", e), + } + })?; + + let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| { + ExtensionError::ValidationError { + reason: format!("Failed to parse haextension.json: {}", e), + } + })?; + + (config.dev.host, config.dev.port) + } else { + // Default values if config doesn't exist + (default_host(), default_port()) + }; + + let dev_server_url = format!("http://{}:{}", host, port); + eprintln!("📡 Dev server URL: {}", dev_server_url); + + // 1.5. Check if dev server is running + if !check_dev_server_health(&dev_server_url).await { + return Err(ExtensionError::ValidationError { + reason: format!( + "Dev server at {} is not reachable. Please start your dev server first (e.g., 'npm run dev')", + dev_server_url + ), + }); + } + eprintln!("✅ Dev server is reachable"); + + // 2. Build path to manifest: /haextension/manifest.json + let manifest_path = extension_path_buf.join("haextension").join("manifest.json"); + + // Check if manifest exists + if !manifest_path.exists() { + return Err(ExtensionError::ManifestError { + reason: format!( + "Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.", + manifest_path.display() + ), + }); + } + + // 3. Read and parse manifest + let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| { + ExtensionError::ManifestError { + reason: format!("Failed to read manifest: {}", e), + } + })?; + + let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; + + // 4. Generate a unique ID for dev extension: dev__ + let key_prefix = manifest + .public_key + .chars() + .take(8) + .collect::(); + let extension_id = format!("dev_{}_{}", key_prefix, manifest.name); + + // 5. Check if dev extension already exists (allow reload) + if let Some(existing) = state + .extension_manager + .get_extension_by_public_key_and_name(&manifest.public_key, &manifest.name)? + { + // If it's already a dev extension, remove it first (to allow reload) + if let ExtensionSource::Development { .. } = &existing.source { + state + .extension_manager + .remove_extension(&manifest.public_key, &manifest.name)?; + } + // Note: Production extensions can coexist with dev extensions + // Dev extensions have priority during lookup + } + + // 6. Create dev extension + let extension = Extension { + id: extension_id.clone(), + source: ExtensionSource::Development { + dev_server_url: dev_server_url.clone(), + manifest_path: manifest_path.clone(), + auto_reload: true, + }, + manifest: manifest.clone(), + enabled: true, + last_accessed: SystemTime::now(), + }; + + // 7. Add to dev extensions (no database entry for dev extensions) + state.extension_manager.add_dev_extension(extension)?; + + eprintln!( + "✅ Dev extension loaded: {} v{} ({})", + manifest.name, manifest.version, dev_server_url + ); + + Ok(extension_id) +} + +#[tauri::command] +pub fn remove_dev_extension( + public_key: String, + name: String, + state: State<'_, AppState>, +) -> Result<(), ExtensionError> { + // Only remove from dev_extensions, not production_extensions + let mut dev_exts = state + .extension_manager + .dev_extensions + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })?; + + // Find and remove by public_key and name + let to_remove = dev_exts + .iter() + .find(|(_, ext)| ext.manifest.public_key == public_key && ext.manifest.name == name) + .map(|(id, _)| id.clone()); + + if let Some(id) = to_remove { + dev_exts.remove(&id); + eprintln!("✅ Dev extension removed: {}", name); + Ok(()) + } else { + Err(ExtensionError::NotFound { + public_key, + name, + }) + } +} + +#[tauri::command] +pub fn get_all_dev_extensions( + state: State<'_, AppState>, +) -> Result, ExtensionError> { + let dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| { + ExtensionError::MutexPoisoned { + reason: e.to_string(), + } + })?; + + let mut extensions = Vec::new(); + for ext in dev_exts.values() { + extensions.push(ExtensionInfoResponse::from_extension(ext)?); + } + + Ok(extensions) +} diff --git a/src-tauri/src/extension/permissions/validator.rs b/src-tauri/src/extension/permissions/validator.rs index e2fd737..009137c 100644 --- a/src-tauri/src/extension/permissions/validator.rs +++ b/src-tauri/src/extension/permissions/validator.rs @@ -12,6 +12,14 @@ use tauri::State; pub struct SqlPermissionValidator; impl SqlPermissionValidator { + /// Prüft ob eine Tabelle zur Extension gehört (basierend auf keyHash Präfix) + /// Format: {keyHash}_{extensionName}_{tableName} + fn is_own_table(extension_id: &str, table_name: &str) -> bool { + // Tabellennamen sind im Format: {keyHash}_{extensionName}_{tableName} + // extension_id ist der keyHash der Extension + table_name.starts_with(&format!("{}_", extension_id)) + } + /// Validiert ein SQL-Statement gegen die Permissions einer Extension pub async fn validate_sql( app_state: &State<'_, AppState>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 57b3ce4..125af6a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -76,12 +76,14 @@ pub fn run() { extension::database::extension_sql_execute, extension::database::extension_sql_select, extension::get_all_extensions, + extension::get_all_dev_extensions, extension::get_extension_info, extension::install_extension_with_permissions, extension::is_extension_installed, + extension::load_dev_extension, extension::preview_extension, + extension::remove_dev_extension, extension::remove_extension, - extension::remove_extension_by_full_id, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/haex/extension/marketplace-card.vue b/src/components/haex/extension/marketplace-card.vue index 07ef5fb..e77c7e6 100644 --- a/src/components/haex/extension/marketplace-card.vue +++ b/src/components/haex/extension/marketplace-card.vue @@ -62,11 +62,12 @@ class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400" >
- {{ t('installed') }} + {{ t('installed') }} + {{ t('installedVersion', { version: extension.installedVersion }) }}
@@ -169,12 +167,16 @@ de: by: von install: Installieren installed: Installiert + installedVersion: 'Installiert (v{version})' + update: Aktualisieren details: Details verified: Verifiziert en: by: by install: Install installed: Installed + installedVersion: 'Installed (v{version})' + update: Update details: Details verified: Verified diff --git a/src/components/haex/pass/card/group.vue b/src/components/haex/pass/card/group.vue deleted file mode 100644 index 8033d2f..0000000 --- a/src/components/haex/pass/card/group.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - - -de: - group: - name: Name - description: Beschreibung - icon: Icon - color: Farbe - -en: - group: - name: Name - description: Description - icon: Icon - color: Color - diff --git a/src/components/haex/pass/card/index.vue b/src/components/haex/pass/card/index.vue deleted file mode 100644 index 66f15df..0000000 --- a/src/components/haex/pass/card/index.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/haex/pass/dialog/deleteItem.vue b/src/components/haex/pass/dialog/deleteItem.vue deleted file mode 100644 index 3d6c58a..0000000 --- a/src/components/haex/pass/dialog/deleteItem.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - - -de: - title: Eintrag löschen - question: Soll der Eintrag "{itemName}" in den Papierkorb verschoben werden? - label: Verschieben - - final: - title: Eintrag endgültig löschen - question: Soll der Eintrag "{itemName}" endgültig gelöscht werden? - label: Löschen - -en: - title: Delete Entry - question: Should the “{itemName}” entry be moved to the recycle bin? - label: Move - - final: - title: Delete entry permanently - question: Should the entry “{itemName}” be permanently deleted? - label: Delete - diff --git a/src/components/haex/pass/dialog/unsavedChanges.vue b/src/components/haex/pass/dialog/unsavedChanges.vue deleted file mode 100644 index 3fcf2dc..0000000 --- a/src/components/haex/pass/dialog/unsavedChanges.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - -de: - title: Nicht gespeicherte Änderungen - question: Sollen die Änderungen verworfen werden? - label: Verwerfen - -en: - title: Unsaved changes - question: Should the changes be discarded? - label: Discard - diff --git a/src/components/haex/pass/group/breadcrumbs.vue b/src/components/haex/pass/group/breadcrumbs.vue deleted file mode 100644 index c8a6232..0000000 --- a/src/components/haex/pass/group/breadcrumbs.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - - - -de: - edit: Bearbeiten - -en: - edit: Edit - diff --git a/src/components/haex/pass/group/composables.ts b/src/components/haex/pass/group/composables.ts deleted file mode 100644 index e021f59..0000000 --- a/src/components/haex/pass/group/composables.ts +++ /dev/null @@ -1,71 +0,0 @@ -export const usePasswordGroup = () => { - const areItemsEqual = ( - groupA: unknown | unknown[] | null, - groupB: unknown | unknown[] | null, - ) => { - console.log('compare values', groupA, groupB) - if (groupA === groupB) return true - - if (Array.isArray(groupA) && Array.isArray(groupB)) { - console.log('compare object arrays', groupA, groupB) - if (groupA.length === groupB.length) return true - - return groupA.some((group, index) => { - return areObjectsEqual(group, groupA[index]) - }) - } - return areObjectsEqual(groupA, groupB) - } - - const deepEqual = (obj1: unknown, obj2: unknown) => { - console.log('compare values', obj1, obj2) - if (obj1 === obj2) return true - - // Null/undefined Check - if (obj1 == null || obj2 == null) return obj1 === obj2 - - // Typ-Check - if (typeof obj1 !== typeof obj2) return false - - // Primitive Typen - if (typeof obj1 !== 'object') return obj1 === obj2 - - // Arrays - if (Array.isArray(obj1) !== Array.isArray(obj2)) return false - - if (Array.isArray(obj1)) { - if (obj1.length !== obj2.length) return false - for (let i = 0; i < obj1.length; i++) { - if (!deepEqual(obj1[i], obj2[i])) return false - } - return true - } - - // Date Objekte - if (obj1 instanceof Date && obj2 instanceof Date) { - return obj1.getTime() === obj2.getTime() - } - - // RegExp Objekte - if (obj1 instanceof RegExp && obj2 instanceof RegExp) { - return obj1.toString() === obj2.toString() - } - - // Objekte - const keys1 = Object.keys(obj1) - const keys2 = Object.keys(obj2) - - if (keys1.length !== keys2.length) return false - - for (const key of keys1) { - if (!keys2.includes(key)) return false - if (!deepEqual(obj1[key], obj2[key])) return false - } - - return true - } - return { - areItemsEqual, - deepEqual, - } -} diff --git a/src/components/haex/pass/group/index.vue b/src/components/haex/pass/group/index.vue deleted file mode 100644 index 6a94b83..0000000 --- a/src/components/haex/pass/group/index.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - -de: - name: Name - description: Beschreibung - icon: Icon - color: Farbe - create: Erstellen - save: Speichern - abort: Abbrechen - title: - create: Gruppe erstellen - edit: Gruppe ändern - -en: - name: Name - description: Description - icon: Icon - color: Color - create: Create - save: Save - abort: Abort - title: - create: Create group - edit: Edit group - diff --git a/src/components/haex/pass/item/details.vue b/src/components/haex/pass/item/details.vue deleted file mode 100644 index c79d17e..0000000 --- a/src/components/haex/pass/item/details.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - - -de: - item: - title: Titel - username: Nutzername - password: Passwort - url: Url - note: Notiz - -en: - item: - title: Title - username: Username - password: Password - url: Url - note: Note - diff --git a/src/components/haex/pass/item/history.vue b/src/components/haex/pass/item/history.vue deleted file mode 100644 index bf85e0c..0000000 --- a/src/components/haex/pass/item/history.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - - -{ - "de": { - "noHistory": "Eintrag wurde bisher nicht geändert", - "changedProperty": "Änderung", - "createdAt": "geändert am", - "newValue": "neuer Wert", - "oldValue": "alter Wert", - "password": "Passwort", - "title": "Titel", - "url": "Url", - "username": "Nutzername" - }, - "en": { - "noHistory": "No changes so far", - "changedProperty": "Changes", - "createdAt": "changed at", - "newValue": "new Value", - "oldValue": "old Value", - "password": "Password", - "title": "Title", - "url": "Url", - "username": "Username" - } -} - diff --git a/src/components/haex/pass/item/index.vue b/src/components/haex/pass/item/index.vue deleted file mode 100644 index 3ca1883..0000000 --- a/src/components/haex/pass/item/index.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - - - -{ - "de": { - "create": "Anlegen", - "abort": "Abbrechen", - "tab": { - "details": "Details", - "keyValue": "Extra", - "history": "Verlauf" - } - }, - "en": { - "create": "Create", - "abort": "Abort", - "tab": { - "details": "Details", - "keyValue": "Extra", - "history": "History" - } - } -} - diff --git a/src/components/haex/pass/item/keyValue.vue b/src/components/haex/pass/item/keyValue.vue deleted file mode 100644 index 63c2e98..0000000 --- a/src/components/haex/pass/item/keyValue.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - - -de: - add: Hinzufügen - key: Schlüssel - value: Wert - -en: - add: Add - key: Key - value: Value - diff --git a/src/components/haex/pass/menu/bottom.vue b/src/components/haex/pass/menu/bottom.vue deleted file mode 100644 index fdf3672..0000000 --- a/src/components/haex/pass/menu/bottom.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - -de: - save: Speichern - abort: Abbrechen - edit: Bearbeiten - readonly: Lesemodus - delete: Löschen - -en: - save: Save - abort: Abort - edit: Edit - readonly: Read Mode - delete: Delete - diff --git a/src/components/haex/pass/mobile/menu/index.vue b/src/components/haex/pass/mobile/menu/index.vue deleted file mode 100644 index 4d33520..0000000 --- a/src/components/haex/pass/mobile/menu/index.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - diff --git a/src/components/haex/pass/mobile/menu/item.vue b/src/components/haex/pass/mobile/menu/item.vue deleted file mode 100644 index 9a5d417..0000000 --- a/src/components/haex/pass/mobile/menu/item.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/src/components/haex/pass/mobile/menu/types.d.ts b/src/components/haex/pass/mobile/menu/types.d.ts deleted file mode 100644 index 10a7f4c..0000000 --- a/src/components/haex/pass/mobile/menu/types.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface IPasswordMenuItem { - color?: string | null - icon: string | null - id: string - name: string | null - type: 'group' | 'item' -} diff --git a/src/components/ui/dropdown/vault.vue b/src/components/ui/dropdown/vault.vue index fb65f80..f711938 100644 --- a/src/components/ui/dropdown/vault.vue +++ b/src/components/ui/dropdown/vault.vue @@ -27,6 +27,11 @@ const items: DropdownMenuItem[] = [ label: t('settings'), to: useLocalePath()({ name: 'settings' }), }, + { + icon: 'mdi:code-braces', + label: t('developer'), + to: useLocalePath()({ name: 'settings-developer' }), + }, { icon: 'tabler:logout', label: t('close'), @@ -39,9 +44,11 @@ const items: DropdownMenuItem[] = [ de: settings: 'Einstellungen' + developer: 'Entwickler' close: 'Vault schließen' en: settings: 'Settings' + developer: 'Developer' close: 'Close Vault' diff --git a/src/composables/extensionMessageHandler.ts b/src/composables/extensionMessageHandler.ts index d7abf6d..054946f 100644 --- a/src/composables/extensionMessageHandler.ts +++ b/src/composables/extensionMessageHandler.ts @@ -1,6 +1,11 @@ // composables/extensionMessageHandler.ts import { invoke } from '@tauri-apps/api/core' import type { IHaexHubExtension } from '~/types/haexhub' +import { + EXTENSION_PROTOCOL_NAME, + EXTENSION_PROTOCOL_PREFIX, +} from '~/config/constants' +import type { Platform } from '@tauri-apps/plugin-os' interface ExtensionRequest { id: string @@ -21,44 +26,119 @@ const extensionToWindowMap = new Map() let contextGetters: { getTheme: () => string getLocale: () => string + getPlatform: () => Platform | undefined } | null = null const registerGlobalMessageHandler = () => { if (globalHandlerRegistered) return - console.log('[Parent Debug] ⭐ NEW VERSION LOADED - NO MORE CONTENTWINDOW ⭐') - window.addEventListener('message', async (event: MessageEvent) => { // Ignore console.forward messages - they're handled elsewhere if (event.data?.type === 'console.forward') { return } - // Finde die Extension für dieses event.source (sandbox-compatible) - let extension = sourceRegistry.get(event.source as Window) + const request = event.data as ExtensionRequest - // If not registered yet, register on first message from this source - if (!extension && iframeRegistry.size === 1) { - // If we only have one iframe, assume this message is from it - const entry = Array.from(iframeRegistry.entries())[0] - if (entry) { - const [_, ext] = entry - const windowSource = event.source as Window - sourceRegistry.set(windowSource, ext) - extensionToWindowMap.set(ext.id, windowSource) - extension = ext + // Find extension by decoding event.origin (works with sandboxed iframes) + // Origin formats: + // - Desktop: haex-extension:// + // - Android: http://haex-extension.localhost (need to check request URL for base64) + let extension: IHaexHubExtension | undefined + + console.log( + '[ExtensionHandler] Received message from origin:', + event.origin, + ) + + // Try to decode extension info from origin + if (event.origin) { + let base64Host: string | null = null + + if (event.origin.startsWith(EXTENSION_PROTOCOL_PREFIX)) { + // Desktop format: haex-extension:// + base64Host = event.origin.replace(EXTENSION_PROTOCOL_PREFIX, '') + console.log( + '[ExtensionHandler] Extracted base64 (custom protocol):', + base64Host, + ) + } else if ( + event.origin === `http://${EXTENSION_PROTOCOL_NAME}.localhost` + ) { + // Android format: http://haex-extension.localhost/{base64} (origin doesn't contain extension info) + // We need to identify extension by iframe source or fallback to single-extension mode + console.log( + `[ExtensionHandler] Android format detected (http://${EXTENSION_PROTOCOL_NAME}.localhost)`, + ) + // Fallback to single iframe mode + if (iframeRegistry.size === 1) { + const entry = Array.from(iframeRegistry.entries())[0] + if (entry) { + const [_, ext] = entry + extension = ext + sourceRegistry.set(event.source as Window, ext) + extensionToWindowMap.set(ext.id, event.source as Window) + } + } + } + + if (base64Host && base64Host !== 'localhost') { + try { + const decodedInfo = JSON.parse(atob(base64Host)) as { + name: string + publicKey: string + version: string + } + + // Find matching extension in registry + for (const [_, ext] of iframeRegistry.entries()) { + if ( + ext.name === decodedInfo.name && + ext.publicKey === decodedInfo.publicKey && + ext.version === decodedInfo.version + ) { + extension = ext + // Register for future lookups + sourceRegistry.set(event.source as Window, ext) + extensionToWindowMap.set(ext.id, event.source as Window) + break + } + } + } catch (e) { + console.error('[ExtensionHandler] Failed to decode origin:', e) + } + } + } + + // Fallback: Try to find extension by event.source (for localhost origin or legacy) + if (!extension) { + extension = sourceRegistry.get(event.source as Window) + + // If not registered yet, register on first message from this source + if (!extension && iframeRegistry.size === 1) { + // If we only have one iframe, assume this message is from it + const entry = Array.from(iframeRegistry.entries())[0] + if (entry) { + const [_, ext] = entry + const windowSource = event.source as Window + sourceRegistry.set(windowSource, ext) + extensionToWindowMap.set(ext.id, windowSource) + extension = ext + } + } else if (extension && !extensionToWindowMap.has(extension.id)) { + // Also register in reverse map for broadcasting + extensionToWindowMap.set(extension.id, event.source as Window) } - } else if (extension && !extensionToWindowMap.has(extension.id)) { - // Also register in reverse map for broadcasting - extensionToWindowMap.set(extension.id, event.source as Window) } if (!extension) { + console.warn( + '[ExtensionHandler] Could not identify extension for message:', + event.origin, + ) return // Message ist nicht von einem registrierten IFrame } - const request = event.data as ExtensionRequest - if (!request.id || !request.method) { console.error('[ExtensionHandler] Invalid extension request:', request) return @@ -87,7 +167,7 @@ const registerGlobalMessageHandler = () => { // Use event.source instead of contentWindow to work with sandboxed iframes // For sandboxed iframes, event.origin is "null" (string), which is not valid for postMessage - const targetOrigin = event.origin === 'null' ? '*' : (event.origin || '*') + const targetOrigin = event.origin === 'null' ? '*' : event.origin || '*' ;(event.source as Window)?.postMessage( { @@ -101,7 +181,7 @@ const registerGlobalMessageHandler = () => { // Use event.source instead of contentWindow to work with sandboxed iframes // For sandboxed iframes, event.origin is "null" (string), which is not valid for postMessage - const targetOrigin = event.origin === 'null' ? '*' : (event.origin || '*') + const targetOrigin = event.origin === 'null' ? '*' : event.origin || '*' ;(event.source as Window)?.postMessage( { @@ -127,12 +207,13 @@ export const useExtensionMessageHandler = ( // Initialize context getters (can use composables here because we're in setup) const { currentTheme } = storeToRefs(useUiStore()) const { locale } = useI18n() - + const { platform } = useDeviceStore() // Store getters for use outside setup context if (!contextGetters) { contextGetters = { getTheme: () => currentTheme.value?.value || 'system', getLocale: () => locale.value, + getPlatform: () => platform, } } @@ -203,9 +284,10 @@ async function handleExtensionMethodAsync( ) { switch (request.method) { case 'extension.getInfo': { - const info = await invoke('get_extension_info', { - extensionId: extension.id, - }) as Record + const info = (await invoke('get_extension_info', { + publicKey: extension.publicKey, + name: extension.name, + })) as Record // Override allowedOrigin with the actual window origin // This fixes the dev-mode issue where Rust returns "tauri://localhost" // but the actual origin is "http://localhost:3003" @@ -364,7 +446,9 @@ async function handleStorageMethodAsync( extension: IHaexHubExtension, ) { const storageKey = `ext_${extension.id}_` - console.log(`[HaexHub Storage] ${request.method} for extension ${extension.id}`) + console.log( + `[HaexHub Storage] ${request.method} for extension ${extension.id}`, + ) switch (request.method) { case 'storage.getItem': { @@ -387,16 +471,18 @@ async function handleStorageMethodAsync( case 'storage.clear': { // Remove only extension-specific keys - const keys = Object.keys(localStorage).filter(k => k.startsWith(storageKey)) - keys.forEach(k => localStorage.removeItem(k)) + const keys = Object.keys(localStorage).filter((k) => + k.startsWith(storageKey), + ) + keys.forEach((k) => localStorage.removeItem(k)) return null } case 'storage.keys': { // Return only extension-specific keys (without prefix) const keys = Object.keys(localStorage) - .filter(k => k.startsWith(storageKey)) - .map(k => k.substring(storageKey.length)) + .filter((k) => k.startsWith(storageKey)) + .map((k) => k.substring(storageKey.length)) return keys } diff --git a/src/pages/vault/[vaultId]/extensions/[extensionId].vue b/src/pages/vault/[vaultId]/extensions/[extensionId].vue index adcf95b..50b12e6 100644 --- a/src/pages/vault/[vaultId]/extensions/[extensionId].vue +++ b/src/pages/vault/[vaultId]/extensions/[extensionId].vue @@ -57,14 +57,58 @@ :style="{ display: tab.isVisible && !showConsole ? 'block' : 'none' }" class="absolute inset-0" > + +
+
+ +

+ {{ t('devServer.notReachable.title') }} +

+

+ {{ + t('devServer.notReachable.description', { + url: tab.extension.devServerUrl, + }) + }} +

+
+

+ {{ t('devServer.notReachable.howToStart') }} +

+ cd /path/to/extension + npm run dev +
+ +
+
+