Update to SDK v1.9.10 with centralized method names

- Updated @haexhub/sdk dependency from 1.9.7 to 1.9.10
- Imported HAEXTENSION_METHODS and HAEXTENSION_EVENTS from SDK
- Updated all handler files to use new nested method constants
- Updated extensionMessageHandler to route using constants
- Changed application.open routing in web handler
- All method names now use haextension:subject:action schema
This commit is contained in:
2025-11-14 10:22:52 +01:00
parent 2202415441
commit c1ee8e6bc0
59 changed files with 3294 additions and 472 deletions

View File

@ -1,10 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DbAction } from "./DbAction";
import type { FsAction } from "./FsAction";
import type { HttpAction } from "./HttpAction";
import type { ShellAction } from "./ShellAction";
import type { WebAction } from "./WebAction";
/**
* Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
*/
export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Http": HttpAction } | { "Shell": ShellAction };
export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Web": WebAction } | { "Shell": ShellAction };

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DisplayMode = "auto" | "window" | "iframe";

View File

@ -3,4 +3,4 @@
/**
* Error codes for frontend handling
*/
export type ExtensionErrorCode = "SecurityViolation" | "NotFound" | "PermissionDenied" | "MutexPoisoned" | "Database" | "Filesystem" | "FilesystemWithPath" | "Http" | "Shell" | "Manifest" | "Validation" | "InvalidPublicKey" | "InvalidSignature" | "InvalidActionString" | "SignatureVerificationFailed" | "CalculateHash" | "Installation";
export type ExtensionErrorCode = "SecurityViolation" | "NotFound" | "PermissionDenied" | "MutexPoisoned" | "Database" | "Filesystem" | "FilesystemWithPath" | "Http" | "Web" | "Shell" | "Manifest" | "Validation" | "InvalidPublicKey" | "InvalidSignature" | "InvalidActionString" | "SignatureVerificationFailed" | "CalculateHash" | "Installation";

View File

@ -1,3 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DisplayMode } from "./DisplayMode";
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, entry: string | null, singleInstance: boolean | null, devServerUrl: 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, entry: string | null, singleInstance: boolean | null, displayMode: DisplayMode | null, devServerUrl: string | null, };

View File

@ -1,4 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DisplayMode } from "./DisplayMode";
import type { ExtensionPermissions } from "./ExtensionPermissions";
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, };
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, display_mode: DisplayMode | null, };

View File

@ -1,7 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DbConstraints } from "./DbConstraints";
import type { FsConstraints } from "./FsConstraints";
import type { HttpConstraints } from "./HttpConstraints";
import type { ShellConstraints } from "./ShellConstraints";
import type { WebConstraints } from "./WebConstraints";
export type PermissionConstraints = DbConstraints | FsConstraints | HttpConstraints | ShellConstraints;
export type PermissionConstraints = DbConstraints | FsConstraints | WebConstraints | ShellConstraints;

View File

@ -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 ResourceType = "fs" | "http" | "db" | "shell";
export type ResourceType = "fs" | "web" | "db" | "shell";

View File

@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Definiert Aktionen (HTTP-Methoden), die auf Web-Anfragen angewendet werden können.
*/
export type WebAction = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RateLimit } from "./RateLimit";
export type WebConstraints = { methods: Array<string> | null, rate_limit: RateLimit | null, };

View File

@ -1,6 +1,7 @@
mod generator;
fn main() {
generator::event_names::generate_event_names();
generator::table_names::generate_table_names();
generator::rust_types::generate_rust_types();
tauri_build::build();

View File

@ -0,0 +1,16 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "extensions",
"description": "Minimal capability for extension webviews - extensions have NO direct system access",
"local": true,
"webviews": ["ext_*"],
"permissions": [
"core:default",
"core:webview:default",
"notification:default",
"notification:allow-is-permission-granted"
],
"remote": {
"urls": ["http://localhost:*", "haex-extension://*"]
}
}

View File

@ -0,0 +1,10 @@
CREATE TABLE `haex_sync_status` (
`id` text PRIMARY KEY NOT NULL,
`backend_id` text NOT NULL,
`last_pull_sequence` integer,
`last_push_hlc_timestamp` text,
`last_sync_at` text,
`error` text
);
--> statement-breakpoint
ALTER TABLE `haex_extensions` ADD `display_mode` text DEFAULT 'auto';

View File

@ -0,0 +1,903 @@
{
"version": "6",
"dialect": "sqlite",
"id": "7ae230a2-4488-4214-9163-602018852676",
"prevId": "bf82259e-9264-44e7-a60f-8cc14a1f22e2",
"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_sync_status": {
"name": "haex_sync_status",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"backend_id": {
"name": "backend_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"last_pull_sequence": {
"name": "last_pull_sequence",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"last_push_hlc_timestamp": {
"name": "last_push_hlc_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"last_sync_at": {
"name": "last_sync_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"haex_desktop_items": {
"name": "haex_desktop_items",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"item_type": {
"name": "item_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"extension_id": {
"name": "extension_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"system_window_id": {
"name": "system_window_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"position_x": {
"name": "position_x",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"position_y": {
"name": "position_y",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"haex_timestamp": {
"name": "haex_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"haex_desktop_items_workspace_id_haex_workspaces_id_fk": {
"name": "haex_desktop_items_workspace_id_haex_workspaces_id_fk",
"tableFrom": "haex_desktop_items",
"tableTo": "haex_workspaces",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"haex_desktop_items_extension_id_haex_extensions_id_fk": {
"name": "haex_desktop_items_extension_id_haex_extensions_id_fk",
"tableFrom": "haex_desktop_items",
"tableTo": "haex_extensions",
"columnsFrom": [
"extension_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {
"item_reference": {
"name": "item_reference",
"value": "(\"haex_desktop_items\".\"item_type\" = 'extension' AND \"haex_desktop_items\".\"extension_id\" IS NOT NULL AND \"haex_desktop_items\".\"system_window_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'system' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'file' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'folder' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL)"
}
}
},
"haex_devices": {
"name": "haex_devices",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"device_id": {
"name": "device_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"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_timestamp": {
"name": "haex_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"haex_devices_device_id_unique": {
"name": "haex_devices_device_id_unique",
"columns": [
"device_id"
],
"isUnique": true
}
},
"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": true,
"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_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": "cascade",
"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": false,
"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
},
"single_instance": {
"name": "single_instance",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"display_mode": {
"name": "display_mode",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'auto'"
},
"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_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
},
"device_id": {
"name": "device_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"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_timestamp": {
"name": "haex_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"haex_settings_device_id_key_type_unique": {
"name": "haex_settings_device_id_key_type_unique",
"columns": [
"device_id",
"key",
"type"
],
"isUnique": true
}
},
"foreignKeys": {
"haex_settings_device_id_haex_devices_id_fk": {
"name": "haex_settings_device_id_haex_devices_id_fk",
"tableFrom": "haex_settings",
"tableTo": "haex_devices",
"columnsFrom": [
"device_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"haex_sync_backends": {
"name": "haex_sync_backends",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"server_url": {
"name": "server_url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"priority": {
"name": "priority",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"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_timestamp": {
"name": "haex_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"haex_workspaces": {
"name": "haex_workspaces",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"device_id": {
"name": "device_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"position": {
"name": "position",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"background": {
"name": "background",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"haex_timestamp": {
"name": "haex_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"haex_workspaces_position_unique": {
"name": "haex_workspaces_position_unique",
"columns": [
"position"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -29,6 +29,13 @@
"when": 1762300795436,
"tag": "0003_luxuriant_deathstrike",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1762894662424,
"tag": "0004_fast_epoch",
"breakpoints": true
}
]
}

Binary file not shown.

View File

@ -1 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-applocaldata-read-recursive","fs:allow-applocaldata-write-recursive","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-mkdir","fs:allow-exists","fs:allow-remove","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"},{"path":"$TEMP/**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url",{"identifier":"opener:allow-open-path","allow":[{"path":"$TEMP/**"}]},"opener:default","os:allow-hostname","os:default","store:default"]}}
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-applocaldata-read-recursive","fs:allow-applocaldata-write-recursive","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-mkdir","fs:allow-exists","fs:allow-remove","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"},{"path":"$TEMP/**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url",{"identifier":"opener:allow-open-path","allow":[{"path":"$TEMP/**"}]},"opener:default","os:allow-hostname","os:default","store:default"]},"extensions":{"identifier":"extensions","description":"Minimal capability for extension webviews - extensions have NO direct system access","remote":{"urls":["http://localhost:*","haex-extension://*"]},"local":true,"webviews":["ext_*"],"permissions":["core:default","core:webview:default","notification:default","notification:allow-is-permission-granted"]}}

View File

@ -0,0 +1,76 @@
// src-tauri/generator/event_names.rs
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::Path;
#[derive(Debug, Deserialize)]
struct EventNames {
extension: HashMap<String, String>,
}
pub fn generate_event_names() {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
println!("Generiere Event-Namen nach {out_dir}");
let events_path = Path::new("../src/constants/eventNames.json");
let dest_path = Path::new(&out_dir).join("eventNames.rs");
let file = File::open(events_path).expect("Konnte eventNames.json nicht öffnen");
let reader = BufReader::new(file);
let events: EventNames =
serde_json::from_reader(reader).expect("Konnte eventNames.json nicht parsen");
let mut code = String::from(
r#"
// ==================================================================
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
// ==================================================================
"#,
);
// Extension Events
code.push_str("// --- Extension Events ---\n");
for (key, value) in &events.extension {
let const_name = format!("EVENT_EXTENSION_{}", to_screaming_snake_case(key));
code.push_str(&format!(
"pub const {}: &str = \"{}\";\n",
const_name, value
));
}
code.push('\n');
// --- Datei schreiben ---
let mut f = File::create(&dest_path).expect("Konnte Zieldatei nicht erstellen");
f.write_all(code.as_bytes())
.expect("Konnte nicht in Zieldatei schreiben");
println!("cargo:rerun-if-changed=../src/constants/eventNames.json");
}
/// Konvertiert einen String zu SCREAMING_SNAKE_CASE
fn to_screaming_snake_case(s: &str) -> String {
let mut result = String::new();
let mut prev_is_lower = false;
for (i, ch) in s.chars().enumerate() {
if ch == '_' {
result.push('_');
prev_is_lower = false;
} else if ch.is_uppercase() {
if i > 0 && prev_is_lower {
result.push('_');
}
result.push(ch);
prev_is_lower = false;
} else {
result.push(ch.to_ascii_uppercase());
prev_is_lower = true;
}
}
result
}

View File

@ -1,3 +1,4 @@
// build/mod.rs
pub mod event_names;
pub mod rust_types;
pub mod table_names;

View File

@ -2,7 +2,7 @@ use crate::database::core::with_connection;
use crate::database::error::DatabaseError;
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
use crate::extension::core::ExtensionPermissions;
use crate::extension::core::{DisplayMode, ExtensionPermissions};
use crate::extension::crypto::ExtensionCrypto;
use crate::extension::database::executor::SqlExecutor;
use crate::extension::error::ExtensionError;
@ -136,12 +136,16 @@ impl ExtensionManager {
// Fallback 1: Check haextension/favicon.ico
let haextension_favicon = format!("{haextension_dir}/favicon.ico");
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)? {
if let Some(clean_path) =
Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)?
{
return Ok(Some(clean_path.to_string_lossy().to_string()));
}
// Fallback 2: Check public/favicon.ico
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)? {
if let Some(clean_path) =
Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)?
{
return Ok(Some(clean_path.to_string_lossy().to_string()));
}
@ -156,16 +160,20 @@ impl ExtensionManager {
app_handle: &AppHandle,
) -> Result<ExtractedExtension, ExtensionError> {
// Use app_cache_dir for better Android compatibility
let cache_dir = app_handle
.path()
.app_cache_dir()
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot get app cache dir: {e}"),
})?;
let cache_dir =
app_handle
.path()
.app_cache_dir()
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot get app cache dir: {e}"),
})?;
let temp_id = uuid::Uuid::new_v4();
let temp = cache_dir.join(format!("{temp_prefix}_{temp_id}"));
let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp"));
let zip_file_path = cache_dir.join(format!(
"{}_{}_{}.haextension",
temp_prefix, temp_id, "temp"
));
// Write bytes to a temporary ZIP file first (important for Android file system)
fs::write(&zip_file_path, &bytes).map_err(|e| {
@ -181,11 +189,10 @@ impl ExtensionManager {
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
})?;
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
ExtensionError::InstallationFailed {
let mut archive =
ZipArchive::new(zip_file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {e}"),
}
})?;
})?;
archive
.extract(&temp)
@ -199,15 +206,17 @@ impl ExtensionManager {
// Read haextension_dir from config if it exists, otherwise use default
let config_path = temp.join("haextension.config.json");
let haextension_dir = if config_path.exists() {
let config_content = std::fs::read_to_string(&config_path)
.map_err(|e| ExtensionError::ManifestError {
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Cannot read haextension.config.json: {e}"),
})?;
}
})?;
let config: serde_json::Value = serde_json::from_str(&config_content)
.map_err(|e| ExtensionError::ManifestError {
let config: serde_json::Value = serde_json::from_str(&config_content).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Invalid haextension.config.json: {e}"),
})?;
}
})?;
let dir = config
.get("dev")
@ -237,14 +246,19 @@ impl ExtensionManager {
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// Validate and resolve icon path with fallback logic
let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?;
let validated_icon = Self::validate_and_resolve_icon_path(
&actual_dir,
&haextension_dir,
manifest.icon.as_deref(),
)?;
manifest.icon = validated_icon;
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
ExtensionError::SignatureVerificationFailed {
reason: e.to_string(),
}
})?;
let content_hash =
ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
ExtensionError::SignatureVerificationFailed {
reason: e.to_string(),
}
})?;
Ok(ExtractedExtension {
temp_dir: actual_dir,
@ -437,9 +451,7 @@ impl ExtensionManager {
})?;
eprintln!("DEBUG: Removing extension with ID: {}", extension.id);
eprintln!(
"DEBUG: Extension name: {extension_name}, version: {extension_version}"
);
eprintln!("DEBUG: Extension name: {extension_name}, version: {extension_version}");
// Lösche Permissions und Extension-Eintrag in einer Transaktion
with_connection(&state.db, |conn| {
@ -516,7 +528,8 @@ impl ExtensionManager {
app_handle: &AppHandle,
file_bytes: Vec<u8>,
) -> Result<ExtensionPreview, ExtensionError> {
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
let extracted =
Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
let is_valid_signature = ExtensionCrypto::verify_signature(
&extracted.manifest.public_key,
@ -541,7 +554,8 @@ impl ExtensionManager {
custom_permissions: EditablePermissions,
state: &State<'_, AppState>,
) -> Result<String, ExtensionError> {
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
let extracted =
Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
ExtensionCrypto::verify_signature(
@ -612,28 +626,29 @@ impl ExtensionManager {
// 1. Extension-Eintrag erstellen mit generierter UUID
let insert_ext_sql = format!(
"INSERT INTO {TABLE_EXTENSIONS} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT INTO {TABLE_EXTENSIONS} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance, display_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
SqlExecutor::execute_internal_typed(
&tx,
&hlc_service,
&insert_ext_sql,
rusqlite::params![
extension_id,
extracted.manifest.name,
extracted.manifest.version,
extracted.manifest.author,
extracted.manifest.entry,
extracted.manifest.icon,
extracted.manifest.public_key,
extracted.manifest.signature,
extracted.manifest.homepage,
extracted.manifest.description,
true, // enabled
extracted.manifest.single_instance.unwrap_or(false),
],
)?;
&tx,
&hlc_service,
&insert_ext_sql,
rusqlite::params![
extension_id,
extracted.manifest.name,
extracted.manifest.version,
extracted.manifest.author,
extracted.manifest.entry,
extracted.manifest.icon,
extracted.manifest.public_key,
extracted.manifest.signature,
extracted.manifest.homepage,
extracted.manifest.description,
true, // enabled
extracted.manifest.single_instance.unwrap_or(false),
extracted.manifest.display_mode.as_ref().map(|dm| format!("{:?}", dm).to_lowercase()).unwrap_or_else(|| "auto".to_string()),
],
)?;
// 2. Permissions speichern
let insert_perm_sql = format!(
@ -709,7 +724,7 @@ impl ExtensionManager {
// Lade alle Daten aus der Datenbank
let extensions = with_connection(&state.db, |conn| {
let sql = format!(
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {TABLE_EXTENSIONS}"
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance, display_mode FROM {TABLE_EXTENSIONS}"
);
eprintln!("DEBUG: SQL Query before transformation: {sql}");
@ -750,6 +765,11 @@ impl ExtensionManager {
single_instance: row[11]
.as_bool()
.or_else(|| row[11].as_i64().map(|v| v != 0)),
display_mode: row[12].as_str().and_then(|s| match s {
"window" => Some(DisplayMode::Window),
"iframe" => Some(DisplayMode::Iframe),
"auto" | _ => Some(DisplayMode::Auto),
}),
};
let enabled = row[10]
@ -808,14 +828,12 @@ impl ExtensionManager {
match std::fs::read_to_string(&config_path) {
Ok(config_content) => {
match serde_json::from_str::<serde_json::Value>(&config_content) {
Ok(config) => {
config
.get("dev")
.and_then(|dev| dev.get("haextension_dir"))
.and_then(|dir| dir.as_str())
.unwrap_or("haextension")
.to_string()
}
Ok(config) => config
.get("dev")
.and_then(|dev| dev.get("haextension_dir"))
.and_then(|dir| dir.as_str())
.unwrap_or("haextension")
.to_string(),
Err(_) => "haextension".to_string(),
}
}

View File

@ -13,7 +13,8 @@ use ts_rs::TS;
pub struct PermissionEntry {
pub target: String,
/// Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute").
/// Die auszuführende Aktion (z.B. "read", "read_write", "execute").
/// Für Web-Permissions ist dies optional und wird ignoriert.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
@ -51,10 +52,29 @@ pub struct ExtensionPermissions {
/// Typ-Alias für bessere Lesbarkeit, wenn die Struktur als UI-Modell verwendet wird.
pub type EditablePermissions = ExtensionPermissions;
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
#[serde(rename_all = "lowercase")]
pub enum DisplayMode {
/// Platform decides: Desktop = window, Mobile/Web = iframe (default)
Auto,
/// Always open in native window (if available, falls back to iframe)
Window,
/// Always open in iframe (embedded in main app)
Iframe,
}
impl Default for DisplayMode {
fn default() -> Self {
Self::Auto
}
}
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
pub struct ExtensionManifest {
pub name: String,
#[serde(default = "default_version_value")]
pub version: String,
pub author: Option<String>,
#[serde(default = "default_entry_value")]
@ -67,12 +87,18 @@ pub struct ExtensionManifest {
pub description: Option<String>,
#[serde(default)]
pub single_instance: Option<bool>,
#[serde(default)]
pub display_mode: Option<DisplayMode>,
}
fn default_entry_value() -> Option<String> {
Some("index.html".to_string())
}
fn default_version_value() -> String {
"0.0.0-dev".to_string()
}
impl ExtensionManifest {
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
/// indem der Standardstatus `Granted` gesetzt wird.
@ -146,7 +172,14 @@ impl ExtensionPermissions {
ResourceType::Fs => FsAction::from_str(operation_str)
.ok()
.map(Action::Filesystem),
ResourceType::Web => WebAction::from_str(operation_str).ok().map(Action::Web),
ResourceType::Web => {
// For web permissions, operation is optional - default to All
if operation_str.is_empty() {
Some(Action::Web(WebAction::All))
} else {
WebAction::from_str(operation_str).ok().map(Action::Web)
}
}
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
};
@ -181,6 +214,7 @@ pub struct ExtensionInfoResponse {
pub icon: Option<String>,
pub entry: Option<String>,
pub single_instance: Option<bool>,
pub display_mode: Option<DisplayMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dev_server_url: Option<String>,
}
@ -208,6 +242,7 @@ impl ExtensionInfoResponse {
icon: extension.manifest.icon.clone(),
entry: extension.manifest.entry.clone(),
single_instance: extension.manifest.single_instance,
display_mode: extension.manifest.display_mode.clone(),
dev_server_url,
})
}

View File

@ -25,10 +25,10 @@ lazy_static::lazy_static! {
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct ExtensionInfo {
public_key: String,
name: String,
version: String,
pub struct ExtensionInfo {
pub public_key: String,
pub name: String,
pub version: String,
}
#[derive(Debug)]

View File

@ -141,6 +141,11 @@ pub async fn extension_sql_execute(
let mut statement = ast_vec.pop().unwrap();
// If this is a SELECT statement, delegate to extension_sql_select
if matches!(statement, Statement::Query(_)) {
return extension_sql_select(sql, params, public_key, name, state).await;
}
// Check if statement has RETURNING clause
let has_returning = crate::database::core::statement_has_returning(&statement);

View File

@ -15,6 +15,9 @@ pub mod filesystem;
pub mod permissions;
pub mod web;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub mod webview;
#[tauri::command]
pub fn get_extension_info(
public_key: String,
@ -429,3 +432,85 @@ pub fn get_all_dev_extensions(
Ok(extensions)
}
// ============================================================================
// WebviewWindow Commands (Desktop only)
// ============================================================================
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
pub fn open_extension_webview_window(
app_handle: AppHandle,
state: State<'_, AppState>,
extension_id: String,
title: String,
width: f64,
height: f64,
x: Option<f64>,
y: Option<f64>,
) -> Result<String, ExtensionError> {
eprintln!("[open_extension_webview_window] Received extension_id: {}", extension_id);
// Returns the window_id (generated UUID without dashes)
state.extension_webview_manager.open_extension_window(
&app_handle,
&state.extension_manager,
extension_id,
title,
width,
height,
x,
y,
)
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
pub fn close_extension_webview_window(
app_handle: AppHandle,
state: State<'_, AppState>,
window_id: String,
) -> Result<(), ExtensionError> {
state
.extension_webview_manager
.close_extension_window(&app_handle, &window_id)
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
pub fn focus_extension_webview_window(
app_handle: AppHandle,
state: State<'_, AppState>,
window_id: String,
) -> Result<(), ExtensionError> {
state
.extension_webview_manager
.focus_extension_window(&app_handle, &window_id)
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
pub fn update_extension_webview_window_position(
app_handle: AppHandle,
state: State<'_, AppState>,
window_id: String,
x: f64,
y: f64,
) -> Result<(), ExtensionError> {
state
.extension_webview_manager
.update_extension_window_position(&app_handle, &window_id, x, y)
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
pub fn update_extension_webview_window_size(
app_handle: AppHandle,
state: State<'_, AppState>,
window_id: String,
width: f64,
height: f64,
) -> Result<(), ExtensionError> {
state
.extension_webview_manager
.update_extension_window_size(&app_handle, &window_id, width, height)
}

View File

@ -1,18 +1,18 @@
// src-tauri/src/extension/permissions/commands.rs
// src-tauri/src/extension/permissions/check.rs
use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager;
use crate::AppState;
use std::path::Path;
use tauri::State;
#[tauri::command]
pub async fn check_web_permission(
extension_id: String,
method: String,
url: String,
state: State<'_, AppState>,
) -> Result<(), ExtensionError> {
PermissionManager::check_web_permission(&state, &extension_id, &method, &url).await
PermissionManager::check_web_permission(&state, &extension_id, &url).await
}
#[tauri::command]
@ -60,5 +60,6 @@ pub async fn check_filesystem_permission(
}
};
PermissionManager::check_filesystem_permission(&state, &extension_id, action, &path).await
let file_path = Path::new(&path);
PermissionManager::check_filesystem_permission(&state, &extension_id, action, file_path).await
}

View File

@ -2,12 +2,14 @@ use crate::table_names::TABLE_EXTENSION_PERMISSIONS;
use crate::AppState;
use crate::database::core::with_connection;
use crate::database::error::DatabaseError;
use crate::extension::core::types::ExtensionSource;
use crate::extension::database::executor::SqlExecutor;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionConstraints, PermissionStatus, ResourceType};
use tauri::State;
use crate::database::generated::HaexExtensionPermissions;
use rusqlite::params;
use std::path::Path;
pub struct PermissionManager;
@ -246,30 +248,55 @@ impl PermissionManager {
}
/// Prüft Web-Berechtigungen für Requests
/// Method/operation is not checked - only protocol, domain, port, and path
pub async fn check_web_permission(
app_state: &State<'_, AppState>,
extension_id: &str,
method: &str,
url: &str,
) -> Result<(), ExtensionError> {
// Optimiert: Lade nur Web-Permissions aus der Datenbank
let permissions = with_connection(&app_state.db, |conn| {
let sql = format!(
"SELECT * FROM {TABLE_EXTENSION_PERMISSIONS} WHERE extension_id = ? AND resource_type = 'web'"
);
let mut stmt = conn.prepare(&sql).map_err(DatabaseError::from)?;
// Load permissions - for dev extensions, get from manifest; for production, from database
let permissions = if let Some(extension) = app_state.extension_manager.get_extension(extension_id) {
match &extension.source {
ExtensionSource::Development { .. } => {
// Dev extension - get web permissions from manifest
extension.manifest.permissions
.to_internal_permissions(extension_id)
.into_iter()
.filter(|p| p.resource_type == ResourceType::Web)
.map(|mut p| {
// Dev extensions have all permissions granted by default
p.status = PermissionStatus::Granted;
p
})
.collect()
}
ExtensionSource::Production { .. } => {
// Production extension - load from database
with_connection(&app_state.db, |conn| {
let sql = format!(
"SELECT * FROM {TABLE_EXTENSION_PERMISSIONS} WHERE extension_id = ? AND resource_type = 'web'"
);
let mut stmt = conn.prepare(&sql).map_err(DatabaseError::from)?;
let perms_iter = stmt.query_map(params![extension_id], |row| {
crate::database::generated::HaexExtensionPermissions::from_row(row)
})?;
let perms_iter = stmt.query_map(params![extension_id], |row| {
crate::database::generated::HaexExtensionPermissions::from_row(row)
})?;
let permissions: Vec<ExtensionPermission> = perms_iter
.filter_map(Result::ok)
.map(Into::into)
.collect();
let permissions: Vec<ExtensionPermission> = perms_iter
.filter_map(Result::ok)
.map(Into::into)
.collect();
Ok(permissions)
})?;
Ok(permissions)
})?
}
}
} else {
// Extension not found - deny
return Err(ExtensionError::ValidationError {
reason: format!("Extension not found: {}", extension_id),
});
};
let url_parsed = url::Url::parse(url).map_err(|e| ExtensionError::ValidationError {
reason: format!("Invalid URL: {}", e),
@ -283,37 +310,34 @@ impl PermissionManager {
.iter()
.filter(|perm| perm.status == PermissionStatus::Granted)
.any(|perm| {
let domain_matches = perm.target == "*"
|| perm.target == domain
|| domain.ends_with(&format!(".{}", perm.target));
// Check if target matches the URL
let url_matches = if perm.target == "*" {
// Wildcard matches everything
true
} else if perm.target.contains("://") {
// URL pattern matching (with protocol and optional path)
Self::matches_url_pattern(&perm.target, url)
} else {
// Domain-only matching (legacy behavior)
perm.target == domain || domain.ends_with(&format!(".{}", perm.target))
};
if !domain_matches {
return false;
}
if let Some(PermissionConstraints::Web(constraints)) = &perm.constraints {
if let Some(methods) = &constraints.methods {
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
return false;
}
}
}
true
// Return the URL match result (no method checking)
url_matches
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
method,
&format!("web request to '{}'", url),
"web request",
url,
));
}
Ok(())
}
/* /// Prüft Dateisystem-Berechtigungen
/// Prüft Dateisystem-Berechtigungen
pub async fn check_filesystem_permission(
app_state: &State<'_, AppState>,
extension_id: &str,
@ -423,7 +447,7 @@ impl PermissionManager {
Ok(())
}
*/
// Helper-Methoden - müssen DatabaseError statt ExtensionError zurückgeben
pub fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
match s {
@ -459,6 +483,114 @@ impl PermissionManager {
pattern == path || pattern == "*"
}
/// Matches a URL against a URL pattern
/// Supports:
/// - Path wildcards: "https://domain.com/*"
/// - Subdomain wildcards: "https://*.domain.com/*"
fn matches_url_pattern(pattern: &str, url: &str) -> bool {
// Parse the actual URL
let Ok(url_parsed) = url::Url::parse(url) else {
return false;
};
// Check if pattern contains subdomain wildcard
let has_subdomain_wildcard = pattern.contains("://*.") || pattern.starts_with("*.");
if has_subdomain_wildcard {
// Extract components for wildcard matching
// Pattern: "https://*.example.com/*"
// Get protocol from pattern
let protocol_end = pattern.find("://").unwrap_or(0);
let pattern_protocol = if protocol_end > 0 {
&pattern[..protocol_end]
} else {
""
};
// Protocol must match if specified
if !pattern_protocol.is_empty() && pattern_protocol != url_parsed.scheme() {
return false;
}
// Extract the domain pattern (after *. )
let domain_start = if pattern.contains("://*.") {
pattern.find("://*.").unwrap() + 5 // length of "://.*"
} else if pattern.starts_with("*.") {
2 // length of "*."
} else {
return false;
};
// Find where the domain pattern ends (at / or end of string)
let domain_pattern_end = pattern[domain_start..].find('/').map(|i| domain_start + i).unwrap_or(pattern.len());
let domain_pattern = &pattern[domain_start..domain_pattern_end];
// Check if the URL's host ends with the domain pattern
let Some(url_host) = url_parsed.host_str() else {
return false;
};
// Match: *.example.com should match subdomain.example.com but not example.com
// Also match: exact domain if no subdomain wildcard prefix
if !url_host.ends_with(domain_pattern) && url_host != domain_pattern {
return false;
}
// For subdomain wildcard, ensure there's actually a subdomain
if pattern.contains("*.") && url_host == domain_pattern {
return false; // *.example.com should NOT match example.com
}
// Check path wildcard if present
if pattern.contains("/*") {
// Any path is allowed
return true;
}
// Check exact path if no wildcard
let pattern_path_start = domain_pattern_end;
if pattern_path_start < pattern.len() {
let pattern_path = &pattern[pattern_path_start..];
return url_parsed.path() == pattern_path;
}
return true;
}
// No subdomain wildcard - parse as full URL
let Ok(pattern_url) = url::Url::parse(pattern) else {
return false;
};
// Protocol must match
if pattern_url.scheme() != url_parsed.scheme() {
return false;
}
// Host must match
if pattern_url.host_str() != url_parsed.host_str() {
return false;
}
// Port must match (if specified)
if pattern_url.port() != url_parsed.port() {
return false;
}
// Path matching with wildcard support
if pattern.contains("/*") {
// Extract the base path before the wildcard
if let Some(wildcard_pos) = pattern.find("/*") {
let pattern_before_wildcard = &pattern[..wildcard_pos];
return url.starts_with(pattern_before_wildcard);
}
}
// Exact path match (no wildcard)
pattern_url.path() == url_parsed.path()
}
}

View File

@ -62,11 +62,10 @@ pub async fn extension_web_open(
});
}
// Check web permissions (open uses GET method for permission check)
// Check web permissions
crate::extension::permissions::manager::PermissionManager::check_web_permission(
&state,
&extension.id,
"GET",
&url,
)
.await?;
@ -105,7 +104,6 @@ pub async fn extension_web_fetch(
crate::extension::permissions::manager::PermissionManager::check_web_permission(
&state,
&extension.id,
method_str,
&url,
)
.await?;

View File

@ -0,0 +1,66 @@
use crate::extension::database::{extension_sql_execute, extension_sql_select};
use crate::extension::error::ExtensionError;
use crate::AppState;
use tauri::{State, WebviewWindow};
use super::helpers::get_extension_id;
#[tauri::command]
pub async fn webview_extension_db_query(
window: WebviewWindow,
state: State<'_, AppState>,
query: String,
params: Vec<serde_json::Value>,
) -> Result<serde_json::Value, ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
// Get extension to retrieve public_key and name for existing database functions
let extension = state
.extension_manager
.get_extension(&extension_id)
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Extension with ID {} not found", extension_id),
})?;
let rows = extension_sql_select(&query, params, extension.manifest.public_key.clone(), extension.manifest.name.clone(), state)
.await
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Database query failed: {}", e),
})?;
Ok(serde_json::json!({
"rows": rows,
"rowsAffected": 0,
"lastInsertId": null
}))
}
#[tauri::command]
pub async fn webview_extension_db_execute(
window: WebviewWindow,
state: State<'_, AppState>,
query: String,
params: Vec<serde_json::Value>,
) -> Result<serde_json::Value, ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
// Get extension to retrieve public_key and name for existing database functions
let extension = state
.extension_manager
.get_extension(&extension_id)
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Extension with ID {} not found", extension_id),
})?;
let rows = extension_sql_execute(&query, params, extension.manifest.public_key.clone(), extension.manifest.name.clone(), state)
.await
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Database execute failed: {}", e),
})?;
Ok(serde_json::json!({
"rows": rows,
"rowsAffected": rows.len(),
"lastInsertId": null
}))
}

View File

@ -0,0 +1,113 @@
use crate::extension::error::ExtensionError;
use crate::AppState;
use serde::{Deserialize, Serialize};
use tauri::{State, WebviewWindow};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
#[derive(Debug, Clone, Deserialize)]
pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SaveFileResult {
pub path: String,
pub success: bool,
}
#[tauri::command]
pub async fn webview_extension_fs_save_file(
window: WebviewWindow,
_state: State<'_, AppState>,
data: Vec<u8>,
default_path: Option<String>,
title: Option<String>,
filters: Option<Vec<FileFilter>>,
) -> Result<Option<SaveFileResult>, ExtensionError> {
eprintln!("[Filesystem] save_file called with {} bytes", data.len());
eprintln!("[Filesystem] save_file default_path: {:?}", default_path);
eprintln!("[Filesystem] save_file first 10 bytes: {:?}", &data[..data.len().min(10)]);
// Build save dialog
let mut dialog = window.dialog().file();
if let Some(path) = default_path {
dialog = dialog.set_file_name(&path);
}
if let Some(t) = title {
dialog = dialog.set_title(&t);
}
if let Some(f) = filters {
for filter in f {
let ext_refs: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
dialog = dialog.add_filter(&filter.name, &ext_refs);
}
}
// Show dialog (blocking_save_file is safe in async commands)
eprintln!("[Filesystem] Showing save dialog...");
let file_path = dialog.blocking_save_file();
if let Some(file_path) = file_path {
// Convert FilePath to PathBuf
let path_buf = file_path.as_path().ok_or_else(|| ExtensionError::ValidationError {
reason: "Failed to get file path".to_string(),
})?;
eprintln!("[Filesystem] User selected path: {}", path_buf.display());
eprintln!("[Filesystem] Writing {} bytes to file...", data.len());
// Write file using std::fs
std::fs::write(path_buf, &data)
.map_err(|e| {
eprintln!("[Filesystem] ERROR writing file: {}", e);
ExtensionError::ValidationError {
reason: format!("Failed to write file: {}", e),
}
})?;
eprintln!("[Filesystem] File written successfully");
Ok(Some(SaveFileResult {
path: path_buf.to_string_lossy().to_string(),
success: true,
}))
} else {
eprintln!("[Filesystem] User cancelled");
// User cancelled
Ok(None)
}
}
#[tauri::command]
pub async fn webview_extension_fs_open_file(
window: WebviewWindow,
_state: State<'_, AppState>,
data: Vec<u8>,
file_name: String,
) -> Result<serde_json::Value, ExtensionError> {
// Get temp directory
let temp_dir = std::env::temp_dir();
let temp_file_path = temp_dir.join(&file_name);
// Write file to temp directory using std::fs
std::fs::write(&temp_file_path, data)
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to write temp file: {}", e),
})?;
// Open file with system's default viewer
let path_str = temp_file_path.to_string_lossy().to_string();
window.opener().open_path(path_str, None::<String>)
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to open file: {}", e),
})?;
Ok(serde_json::json!({
"success": true
}))
}

View File

@ -0,0 +1,57 @@
use crate::extension::core::protocol::ExtensionInfo;
use crate::extension::error::ExtensionError;
use crate::AppState;
use tauri::{State, WebviewWindow};
/// Get extension_id from window (SECURITY: window_id from Tauri, cannot be spoofed)
pub fn get_extension_id(window: &WebviewWindow, state: &State<AppState>) -> Result<String, ExtensionError> {
let window_id = window.label();
eprintln!("[webview_api] Looking up extension_id for window: {}", window_id);
let windows = state
.extension_webview_manager
.windows
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
eprintln!("[webview_api] HashMap contents: {:?}", *windows);
let extension_id = windows
.get(window_id)
.cloned()
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Window {} is not registered as an extension window", window_id),
})?;
eprintln!("[webview_api] Found extension_id: {}", extension_id);
Ok(extension_id)
}
/// Get full extension info (public_key, name, version) from window
pub fn get_extension_info_from_window(
window: &WebviewWindow,
state: &State<AppState>,
) -> Result<ExtensionInfo, ExtensionError> {
let extension_id = get_extension_id(window, state)?;
// Get extension from ExtensionManager using the database UUID
let extension = state
.extension_manager
.get_extension(&extension_id)
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Extension with ID {} not found", extension_id),
})?;
let version = match &extension.source {
crate::extension::core::types::ExtensionSource::Production { version, .. } => version.clone(),
crate::extension::core::types::ExtensionSource::Development { .. } => "dev".to_string(),
};
Ok(ExtensionInfo {
public_key: extension.manifest.public_key,
name: extension.manifest.name,
version,
})
}

View File

@ -0,0 +1,302 @@
use crate::event_names::EVENT_EXTENSION_WINDOW_CLOSED;
use crate::extension::error::ExtensionError;
use crate::extension::ExtensionManager;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
/// Verwaltet native WebviewWindows für Extensions (nur Desktop-Plattformen)
pub struct ExtensionWebviewManager {
/// Map: window_id -> extension_id
/// Das window_id ist ein eindeutiger Identifier (Tauri-kompatibel, keine Bindestriche)
/// und wird gleichzeitig als Tauri WebviewWindow label verwendet
pub windows: Arc<Mutex<HashMap<String, String>>>,
}
impl ExtensionWebviewManager {
pub fn new() -> Self {
Self {
windows: Arc::new(Mutex::new(HashMap::new())),
}
}
/// Öffnet eine Extension in einem nativen WebviewWindow
///
/// # Arguments
/// * `app_handle` - Tauri AppHandle
/// * `extension_manager` - Extension Manager für Zugriff auf Extension-Daten
/// * `extension_id` - ID der zu öffnenden Extension
/// * `title` - Fenstertitel
/// * `width` - Fensterbreite
/// * `height` - Fensterhöhe
/// * `x` - X-Position (optional)
/// * `y` - Y-Position (optional)
///
/// # Returns
/// Das window_id des erstellten Fensters
pub fn open_extension_window(
&self,
app_handle: &AppHandle,
extension_manager: &ExtensionManager,
extension_id: String,
title: String,
width: f64,
height: f64,
x: Option<f64>,
y: Option<f64>,
) -> Result<String, ExtensionError> {
// Extension aus Manager holen
let extension = extension_manager
.get_extension(&extension_id)
.ok_or_else(|| ExtensionError::NotFound {
public_key: "".to_string(),
name: extension_id.clone(),
})?;
// URL für Extension generieren (analog zum Frontend)
use crate::extension::core::types::ExtensionSource;
let url = match &extension.source {
ExtensionSource::Production { .. } => {
// Für Production Extensions: custom protocol
#[cfg(target_os = "android")]
let protocol = "http";
#[cfg(not(target_os = "android"))]
let protocol = "haex-extension";
// Extension Info Base64-codieren (wie im Frontend)
let extension_info = serde_json::json!({
"publicKey": extension.manifest.public_key,
"name": extension.manifest.name,
"version": match &extension.source {
ExtensionSource::Production { version, .. } => version,
_ => "",
}
});
let extension_info_str = serde_json::to_string(&extension_info)
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to serialize extension info: {}", e),
})?;
let extension_info_base64 =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, extension_info_str.as_bytes());
#[cfg(target_os = "android")]
let host = "haex-extension.localhost";
#[cfg(not(target_os = "android"))]
let host = "localhost";
let entry = extension.manifest.entry.as_deref().unwrap_or("index.html");
format!("{}://{}/{}/{}", protocol, host, extension_info_base64, entry)
}
ExtensionSource::Development { dev_server_url, .. } => {
// Für Dev Extensions: direkt Dev-Server URL
dev_server_url.clone()
}
};
// Eindeutige Window-ID generieren (wird auch als Tauri label verwendet, keine Bindestriche erlaubt)
let window_id = format!("ext_{}", uuid::Uuid::new_v4().simple());
eprintln!("Opening extension window: {} with URL: {}", window_id, url);
// WebviewWindow erstellen
let webview_url = WebviewUrl::External(url.parse().map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Invalid URL: {}", e),
}
})?);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let mut builder = WebviewWindowBuilder::new(app_handle, &window_id, webview_url)
.title(&title)
.inner_size(width, height)
.decorations(true) // Native Decorations (Titlebar, etc.)
.resizable(true)
.skip_taskbar(false) // In Taskbar anzeigen
.center(); // Fenster zentrieren
#[cfg(any(target_os = "android", target_os = "ios"))]
let mut builder = WebviewWindowBuilder::new(app_handle, &window_id, webview_url)
.inner_size(width, height);
// Position setzen, falls angegeben (nur Desktop)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let (Some(x_pos), Some(y_pos)) = (x, y) {
builder = builder.position(x_pos, y_pos);
}
// Fenster erstellen
let webview_window = builder.build().map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to create webview window: {}", e),
})?;
// Event-Listener für das Schließen des Fensters registrieren
let window_id_for_event = window_id.clone();
let app_handle_for_event = app_handle.clone();
let windows_for_event = self.windows.clone();
webview_window.on_window_event(move |event| {
if let tauri::WindowEvent::Destroyed = event {
eprintln!("WebviewWindow destroyed: {}", window_id_for_event);
// Registry cleanup
if let Ok(mut windows) = windows_for_event.lock() {
windows.remove(&window_id_for_event);
}
// Emit event an Frontend, damit das Tracking aktualisiert wird
let _ = app_handle_for_event.emit(EVENT_EXTENSION_WINDOW_CLOSED, &window_id_for_event);
}
});
// In Registry speichern
let mut windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
windows.insert(window_id.clone(), extension_id.clone());
eprintln!("Extension window opened successfully: {}", window_id);
Ok(window_id)
}
/// Schließt ein Extension-Fenster
pub fn close_extension_window(
&self,
app_handle: &AppHandle,
window_id: &str,
) -> Result<(), ExtensionError> {
let mut windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
if windows.remove(window_id).is_some() {
drop(windows); // Release lock before potentially blocking operation
// Webview Window schließen (nur Desktop)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(window) = app_handle.get_webview_window(window_id) {
window.close().map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to close window: {}", e),
})?;
}
eprintln!("Extension window closed: {}", window_id);
Ok(())
} else {
Err(ExtensionError::NotFound {
public_key: "".to_string(),
name: window_id.to_string(),
})
}
}
/// Fokussiert ein Extension-Fenster
pub fn focus_extension_window(
&self,
app_handle: &AppHandle,
window_id: &str,
) -> Result<(), ExtensionError> {
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
let exists = windows.contains_key(window_id);
drop(windows); // Release lock
if exists {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(window) = app_handle.get_webview_window(window_id) {
window.set_focus().map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to focus window: {}", e),
})?;
// Zusätzlich nach vorne bringen
window.set_always_on_top(true).ok();
window.set_always_on_top(false).ok();
}
Ok(())
} else {
Err(ExtensionError::NotFound {
public_key: "".to_string(),
name: window_id.to_string(),
})
}
}
/// Aktualisiert Position eines Extension-Fensters
pub fn update_extension_window_position(
&self,
app_handle: &AppHandle,
window_id: &str,
x: f64,
y: f64,
) -> Result<(), ExtensionError> {
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
let exists = windows.contains_key(window_id);
drop(windows); // Release lock
if exists {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(window) = app_handle.get_webview_window(window_id) {
use tauri::Position;
window
.set_position(Position::Physical(tauri::PhysicalPosition {
x: x as i32,
y: y as i32,
}))
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to set window position: {}", e),
})?;
}
Ok(())
} else {
Err(ExtensionError::NotFound {
public_key: "".to_string(),
name: window_id.to_string(),
})
}
}
/// Aktualisiert Größe eines Extension-Fensters
pub fn update_extension_window_size(
&self,
app_handle: &AppHandle,
window_id: &str,
width: f64,
height: f64,
) -> Result<(), ExtensionError> {
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?;
let exists = windows.contains_key(window_id);
drop(windows); // Release lock
if exists {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(window) = app_handle.get_webview_window(window_id) {
use tauri::Size;
window
.set_size(Size::Physical(tauri::PhysicalSize {
width: width as u32,
height: height as u32,
}))
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to set window size: {}", e),
})?;
}
Ok(())
} else {
Err(ExtensionError::NotFound {
public_key: "".to_string(),
name: window_id.to_string(),
})
}
}
}
impl Default for ExtensionWebviewManager {
fn default() -> Self {
Self::new()
}
}

View File

@ -0,0 +1,8 @@
pub mod database;
pub mod filesystem;
pub mod helpers;
pub mod manager;
pub mod web;
// Re-export manager types
pub use manager::ExtensionWebviewManager;

View File

@ -0,0 +1,246 @@
use crate::extension::core::protocol::ExtensionInfo;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager;
use crate::extension::permissions::types::{Action, DbAction, FsAction};
use crate::AppState;
use base64::Engine;
use serde::{Deserialize, Serialize};
use tauri::{State, WebviewWindow};
use tauri_plugin_http::reqwest;
use super::helpers::{get_extension_id, get_extension_info_from_window};
// ============================================================================
// Types for SDK communication
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplicationContext {
pub theme: String,
pub locale: String,
pub platform: String,
}
// ============================================================================
// Extension Info Command
// ============================================================================
#[tauri::command]
pub fn webview_extension_get_info(
window: WebviewWindow,
state: State<'_, AppState>,
) -> Result<ExtensionInfo, ExtensionError> {
get_extension_info_from_window(&window, &state)
}
// ============================================================================
// Context API Commands
// ============================================================================
#[tauri::command]
pub fn webview_extension_context_get(
state: State<'_, AppState>,
) -> Result<ApplicationContext, ExtensionError> {
let context = state.context.lock().map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to lock context: {}", e),
})?;
Ok(context.clone())
}
#[tauri::command]
pub fn webview_extension_context_set(
state: State<'_, AppState>,
context: ApplicationContext,
) -> Result<(), ExtensionError> {
let mut current_context = state.context.lock().map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to lock context: {}", e),
})?;
*current_context = context;
Ok(())
}
// ============================================================================
// Permission API Commands
// ============================================================================
#[tauri::command]
pub async fn webview_extension_check_web_permission(
window: WebviewWindow,
state: State<'_, AppState>,
url: String,
) -> Result<bool, ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
match PermissionManager::check_web_permission(&state, &extension_id, &url).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
#[tauri::command]
pub async fn webview_extension_check_database_permission(
window: WebviewWindow,
state: State<'_, AppState>,
resource: String,
operation: String,
) -> Result<bool, ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
let action = match operation.as_str() {
"read" => Action::Database(DbAction::Read),
"write" => Action::Database(DbAction::ReadWrite),
_ => return Ok(false),
};
match PermissionManager::check_database_permission(&state, &extension_id, action, &resource).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
#[tauri::command]
pub async fn webview_extension_check_filesystem_permission(
window: WebviewWindow,
state: State<'_, AppState>,
path: String,
action_str: String,
) -> Result<bool, ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
let action = match action_str.as_str() {
"read" => Action::Filesystem(FsAction::Read),
"write" => Action::Filesystem(FsAction::ReadWrite),
_ => return Ok(false),
};
let path_buf = std::path::Path::new(&path);
match PermissionManager::check_filesystem_permission(&state, &extension_id, action, path_buf).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
// ============================================================================
// Web API Commands
// ============================================================================
#[tauri::command]
pub async fn webview_extension_web_open(
window: WebviewWindow,
state: State<'_, AppState>,
url: String,
) -> Result<(), ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
// Validate URL format
let parsed_url = url::Url::parse(&url).map_err(|e| ExtensionError::WebError {
reason: format!("Invalid URL: {}", e),
})?;
// Only allow http and https URLs
let scheme = parsed_url.scheme();
if scheme != "http" && scheme != "https" {
return Err(ExtensionError::WebError {
reason: format!("Unsupported URL scheme: {}. Only http and https are allowed.", scheme),
});
}
// Check web permissions
PermissionManager::check_web_permission(&state, &extension_id, &url).await?;
// Open URL in default browser using tauri-plugin-opener
tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| ExtensionError::WebError {
reason: format!("Failed to open URL in browser: {}", e),
})?;
Ok(())
}
#[tauri::command]
pub async fn webview_extension_web_request(
window: WebviewWindow,
state: State<'_, AppState>,
url: String,
method: Option<String>,
headers: Option<serde_json::Value>,
body: Option<String>,
) -> Result<serde_json::Value, ExtensionError> {
let extension_id = get_extension_id(&window, &state)?;
// Check permission first
PermissionManager::check_web_permission(&state, &extension_id, &url).await?;
// Build request
let method = method.unwrap_or_else(|| "GET".to_string());
let client = reqwest::Client::new();
let mut request = match method.to_uppercase().as_str() {
"GET" => client.get(&url),
"POST" => client.post(&url),
"PUT" => client.put(&url),
"DELETE" => client.delete(&url),
"PATCH" => client.patch(&url),
_ => {
return Err(ExtensionError::ValidationError {
reason: format!("Unsupported HTTP method: {}", method),
})
}
};
// Add headers
if let Some(headers) = headers {
if let Some(headers_obj) = headers.as_object() {
for (key, value) in headers_obj {
if let Some(value_str) = value.as_str() {
request = request.header(key, value_str);
}
}
}
}
// Add body
if let Some(body) = body {
request = request.body(body);
}
// Execute request
let response = request
.send()
.await
.map_err(|e| ExtensionError::ValidationError {
reason: format!("HTTP request failed: {}", e),
})?;
let status = response.status().as_u16();
let headers_map = response.headers().clone();
// Get response body as bytes
let body_bytes = response
.bytes()
.await
.map_err(|e| ExtensionError::ValidationError {
reason: format!("Failed to read response body: {}", e),
})?;
// Encode body as base64
let body_base64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &body_bytes);
// Convert headers to JSON
let mut headers_json = serde_json::Map::new();
for (key, value) in headers_map.iter() {
if let Ok(value_str) = value.to_str() {
headers_json.insert(
key.to_string(),
serde_json::Value::String(value_str.to_string()),
);
}
}
Ok(serde_json::json!({
"status": status,
"headers": headers_json,
"body": body_base64,
"ok": status >= 200 && status < 300
}))
}

View File

@ -4,11 +4,11 @@ mod extension;
use crate::{
crdt::hlc::HlcService,
database::DbConnection,
extension::{
core::ExtensionManager,
webview::ExtensionWebviewManager,
}
extension::core::ExtensionManager,
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::extension::webview::ExtensionWebviewManager;
use std::sync::{Arc, Mutex};
use tauri::Manager;
@ -24,7 +24,9 @@ pub struct AppState {
pub db: DbConnection,
pub hlc: Mutex<HlcService>,
pub extension_manager: ExtensionManager,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub extension_webview_manager: ExtensionWebviewManager,
pub context: Arc<Mutex<extension::webview::web::ApplicationContext>>,
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -66,7 +68,13 @@ pub fn run() {
db: DbConnection(Arc::new(Mutex::new(None))),
hlc: Mutex::new(HlcService::new()),
extension_manager: ExtensionManager::new(),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension_webview_manager: ExtensionWebviewManager::new(),
context: Arc::new(Mutex::new(extension::webview::web::ApplicationContext {
theme: "dark".to_string(),
locale: "en".to_string(),
platform: std::env::consts::OS.to_string(),
})),
})
//.manage(ExtensionState::default())
.plugin(tauri_plugin_dialog::init())
@ -105,11 +113,39 @@ pub fn run() {
extension::preview_extension,
extension::remove_dev_extension,
extension::remove_extension,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::open_extension_webview_window,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::close_extension_webview_window,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::focus_extension_webview_window,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::update_extension_webview_window_position,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::update_extension_webview_window_size,
// WebView API commands (for native window extensions, desktop only)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::web::webview_extension_get_info,
extension::webview::web::webview_extension_context_get,
extension::webview::web::webview_extension_context_set,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::database::webview_extension_db_query,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::database::webview_extension_db_execute,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::web::webview_extension_check_web_permission,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::web::webview_extension_check_database_permission,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::web::webview_extension_check_filesystem_permission,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::web::webview_extension_web_open,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::web::webview_extension_web_request,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::filesystem::webview_extension_fs_save_file,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
extension::webview::filesystem::webview_extension_fs_open_file,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -11,6 +11,7 @@
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "haex-hub",