diff --git a/.gitignore b/.gitignore index dc32731..53a7b27 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ dist-ssr .nuxt src-tauri/target nogit* -.claude \ No newline at end of file +.claude +.output \ No newline at end of file diff --git a/src-tauri/bindings/ExtensionInfoResponse.ts b/src-tauri/bindings/ExtensionInfoResponse.ts index e93574b..99a8a94 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, }; +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, }; diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs index eec7aa7..9775cf0 100644 --- a/src-tauri/src/extension/core/manifest.rs +++ b/src-tauri/src/extension/core/manifest.rs @@ -196,6 +196,10 @@ pub struct ExtensionInfoResponse { pub display_name: Option, pub namespace: Option, pub allowed_origin: String, + pub enabled: bool, + pub description: Option, + pub homepage: Option, + pub icon: Option, } impl ExtensionInfoResponse { @@ -204,11 +208,7 @@ impl ExtensionInfoResponse { ) -> Result { use crate::extension::core::types::get_tauri_origin; - // In development mode, use a wildcard for localhost to match any port - #[cfg(debug_assertions)] - let allowed_origin = "http://localhost:3003".to_string(); - - #[cfg(not(debug_assertions))] + // 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()?; @@ -222,6 +222,10 @@ impl ExtensionInfoResponse { display_name: Some(extension.manifest.name.clone()), namespace: extension.manifest.author.clone(), allowed_origin, + enabled: extension.enabled, + description: extension.manifest.description.clone(), + homepage: extension.manifest.homepage.clone(), + icon: extension.manifest.icon.clone(), }) } } diff --git a/src-tauri/src/extension/core/protocol.rs b/src-tauri/src/extension/core/protocol.rs index 438a5d6..70eb56e 100644 --- a/src-tauri/src/extension/core/protocol.rs +++ b/src-tauri/src/extension/core/protocol.rs @@ -260,275 +260,6 @@ pub fn extension_protocol_handler( println!("Path: {}", path_str); println!("Asset to load: {}", asset_to_load); - /* match process_hex_encoded_json(&encoded_info) { - Ok(info) => { - println!("=== Extension Protocol Handler ==="); - println!("Full URI: {}", uri_ref); - println!("Origin: {}", origin); - println!("Encoded Info (aus Origin): {}", encoded_info); - println!("Path: {}", path_str); - println!("Asset to load: {}", asset_to_load); - println!("Decoded info:"); - println!(" KeyHash: {}", info.key_hash); - println!(" Name: {}", info.name); - println!(" Version: {}", info.version); - - let absolute_secure_path = resolve_secure_extension_asset_path( - app_handle, - state, - &info.key_hash, - &info.name, - &info.version, - &asset_to_load, - )?; - - println!("Resolved path: {}", absolute_secure_path.display()); - println!("File exists: {}", absolute_secure_path.exists()); - - if absolute_secure_path.exists() && absolute_secure_path.is_file() { - match fs::read(&absolute_secure_path) { - Ok(mut content) => { - let mime_type = mime_guess::from_path(&absolute_secure_path) - .first_or(mime::APPLICATION_OCTET_STREAM) - .to_string(); - - if asset_to_load == "index.html" && mime_type.contains("html") { - if let Ok(html_str) = String::from_utf8(content.clone()) { - let base_tag = format!(r#""#, encoded_info); - let modified_html = if let Some(head_pos) = html_str.find("") - { - let insert_pos = head_pos + 6; - format!( - "{}{}{}", - &html_str[..insert_pos], - base_tag, - &html_str[insert_pos..] - ) - } else { - // Fallback: Prepend - format!("{}{}", base_tag, html_str) - }; - content = modified_html.into_bytes(); - } - } - // Inject localStorage polyfill for HTML files with caching - if asset_to_load == "index.html" && mime_type.contains("html") { - // Create cache key: extension_id (from host) - let cache_key = format!("{}_{}", host, asset_to_load); - - // Check cache first - if let Ok(cache) = HTML_CACHE.lock() { - if let Some(cached_content) = cache.get(&cache_key) { - println!("Serving cached HTML for: {}", cache_key); - content = cached_content.clone(); - - let content_length = content.len(); - return Response::builder() - .status(200) - .header("Content-Type", mime_type) - .header("Content-Length", content_length.to_string()) - .header("Accept-Ranges", "bytes") - .header("X-HaexHub-Cache", "HIT") - .header("Access-Control-Allow-Origin", allowed_origin) - .header( - "Access-Control-Allow-Methods", - "GET, POST, OPTIONS", - ) - .header("Access-Control-Allow-Headers", "*") - .header("Access-Control-Allow-Credentials", "true") - .body(content) - .map_err(|e| e.into()); - } - } - - // Not in cache, modify and cache it - if let Ok(html_content) = String::from_utf8(content.clone()) { - let polyfill = r#""#; - - // Inject as the FIRST thing in , before any other script - let modified_html = if let Some(head_pos) = - html_content.find("") - { - let insert_pos = head_pos + 6; // After - format!( - "{}{}{}", - &html_content[..insert_pos], - polyfill, - &html_content[insert_pos..] - ) - } else if let Some(charset_pos) = html_content.find("{}{}", - &html_content[..charset_pos], - polyfill, - &html_content[charset_pos..] - ) - } else if let Some(html_pos) = html_content.find("") - { - // Insert right after DOCTYPE - let insert_pos = html_pos + 15; // After - format!( - "{}{}{}", - &html_content[..insert_pos], - polyfill, - &html_content[insert_pos..] - ) - } else { - // Prepend to entire file - format!("{}{}", polyfill, html_content) - }; - - content = modified_html.into_bytes(); - - // Cache the modified HTML - if let Ok(mut cache) = HTML_CACHE.lock() { - cache.insert(cache_key, content.clone()); - println!("Cached modified HTML for future requests"); - } - } - } - - let content_length = content.len(); - println!( - "Liefere {} ({}, {} bytes) ", - absolute_secure_path.display(), - mime_type, - content_length - ); - Response::builder() - .status(200) - .header("Content-Type", &mime_type) - .header("Content-Length", content_length.to_string()) - .header("Accept-Ranges", "bytes") - .header( - "X-HaexHub-Cache", - if asset_to_load == "index.html" && mime_type.contains("html") { - "MISS" - } else { - "N/A" - }, - ) - .header("Access-Control-Allow-Origin", allowed_origin) - .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - .header("Access-Control-Allow-Headers", "*") - .header("Access-Control-Allow-Credentials", "true") - .body(content) - .map_err(|e| e.into()) - } - Err(e) => { - eprintln!( - "Fehler beim Lesen der Datei {}: {}", - absolute_secure_path.display(), - e - ); - let status_code = if e.kind() == std::io::ErrorKind::NotFound { - 404 - } else if e.kind() == std::io::ErrorKind::PermissionDenied { - 403 - } else { - 500 - }; - - Response::builder() - .status(status_code) - .header("Access-Control-Allow-Origin", allowed_origin) - .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - .header("Access-Control-Allow-Headers", "*") - .body(Vec::new()) - .map_err(|e| e.into()) - } - } - } else { - eprintln!( - "Asset nicht gefunden oder ist kein File: {}", - absolute_secure_path.display() - ); - Response::builder() - .status(404) - .header("Access-Control-Allow-Origin", allowed_origin) - .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - .header("Access-Control-Allow-Headers", "*") - .body(Vec::new()) - .map_err(|e| e.into()) - } - } - Err(e) => { - eprintln!("Fehler bei der Datenverarbeitung: {}", e); - - Response::builder() - .status(500) - .header("Access-Control-Allow-Origin", allowed_origin) - .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - .header("Access-Control-Allow-Headers", "*") - .body(Vec::new()) - .map_err(|e| e.into()) - } - } - */ - let absolute_secure_path = resolve_secure_extension_asset_path( app_handle, &state, diff --git a/src-tauri/src/extension/core/types.rs b/src-tauri/src/extension/core/types.rs index ac77169..8d851be 100644 --- a/src-tauri/src/extension/core/types.rs +++ b/src-tauri/src/extension/core/types.rs @@ -47,7 +47,9 @@ pub fn get_tauri_origin() -> String { #[cfg(target_os = "android")] { - "tauri://localhost".to_string() + // On Android, with http://*.localhost URLs, the origin is "null" + // This is a browser security feature for local/file protocols + "null".to_string() } #[cfg(target_os = "ios")] diff --git a/src/components/haex/extension/launcher.vue b/src/components/haex/extension/launcher.vue new file mode 100644 index 0000000..aa70d94 --- /dev/null +++ b/src/components/haex/extension/launcher.vue @@ -0,0 +1,123 @@ + + + + + +de: + disabled: Deaktiviert + marketplace: Marketplace + +en: + disabled: Disabled + marketplace: Marketplace + diff --git a/src/components/haex/extension/marketplace-card.vue b/src/components/haex/extension/marketplace-card.vue index 8b54ede..07ef5fb 100644 --- a/src/components/haex/extension/marketplace-card.vue +++ b/src/components/haex/extension/marketplace-card.vue @@ -57,10 +57,17 @@ {{ extension.description }}

- +
+
+ + {{ t('installed') }} +
- - - - - - - - - - -de: - settings: 'Einstellungen' - close: 'Vault schließen' - -en: - settings: 'Settings' - close: 'Close Vault' - diff --git a/src/composables/extensionContextBroadcast.ts b/src/composables/extensionContextBroadcast.ts index 9e24880..1ce18eb 100644 --- a/src/composables/extensionContextBroadcast.ts +++ b/src/composables/extensionContextBroadcast.ts @@ -1,17 +1,22 @@ // composables/extensionContextBroadcast.ts +// NOTE: This composable is deprecated. Use tabsStore.broadcastToAllTabs() instead. +// Keeping for backwards compatibility. + +import { getExtensionWindow } from './extensionMessageHandler' + export const useExtensionContextBroadcast = () => { - // Globaler State für alle aktiven IFrames - const extensionIframes = useState>( - 'extension-iframes', + // Globaler State für Extension IDs statt IFrames + const extensionIds = useState>( + 'extension-ids', () => new Set(), ) - const registerExtensionIframe = (iframe: HTMLIFrameElement) => { - extensionIframes.value.add(iframe) + const registerExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => { + extensionIds.value.add(extensionId) } - const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => { - extensionIframes.value.delete(iframe) + const unregisterExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => { + extensionIds.value.delete(extensionId) } const broadcastContextChange = (context: { @@ -25,8 +30,11 @@ export const useExtensionContextBroadcast = () => { timestamp: Date.now(), } - extensionIframes.value.forEach((iframe) => { - iframe.contentWindow?.postMessage(message, '*') + extensionIds.value.forEach((extensionId) => { + const win = getExtensionWindow(extensionId) + if (win) { + win.postMessage(message, '*') + } }) } @@ -40,8 +48,11 @@ export const useExtensionContextBroadcast = () => { timestamp: Date.now(), } - extensionIframes.value.forEach((iframe) => { - iframe.contentWindow?.postMessage(message, '*') + extensionIds.value.forEach((extensionId) => { + const win = getExtensionWindow(extensionId) + if (win) { + win.postMessage(message, '*') + } }) } diff --git a/src/composables/extensionMessageHandler.ts b/src/composables/extensionMessageHandler.ts index a9af862..d7abf6d 100644 --- a/src/composables/extensionMessageHandler.ts +++ b/src/composables/extensionMessageHandler.ts @@ -12,6 +12,10 @@ interface ExtensionRequest { // Globaler Handler - nur einmal registriert let globalHandlerRegistered = false const iframeRegistry = new Map() +// Map event.source (WindowProxy) to extension for sandbox-compatible matching +const sourceRegistry = new Map() +// Reverse map: extension ID to Window for broadcasting +const extensionToWindowMap = new Map() // Store context values that need to be accessed outside setup let contextGetters: { @@ -22,36 +26,44 @@ let contextGetters: { const registerGlobalMessageHandler = () => { if (globalHandlerRegistered) return - window.addEventListener('message', async (event: MessageEvent) => { - // Finde die Extension für dieses IFrame - let extension: IHaexHubExtension | undefined - let sourceIframe: HTMLIFrameElement | undefined + console.log('[Parent Debug] ⭐ NEW VERSION LOADED - NO MORE CONTENTWINDOW ⭐') - for (const [iframe, ext] of iframeRegistry.entries()) { - if (event.source === iframe.contentWindow) { - extension = ext - sourceIframe = iframe - break - } + window.addEventListener('message', async (event: MessageEvent) => { + // Ignore console.forward messages - they're handled elsewhere + if (event.data?.type === 'console.forward') { + return } - if (!extension || !sourceIframe) { + // Finde die Extension für dieses event.source (sandbox-compatible) + let 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) + } + + if (!extension) { return // Message ist nicht von einem registrierten IFrame } const request = event.data as ExtensionRequest if (!request.id || !request.method) { - console.error('Invalid extension request:', request) + console.error('[ExtensionHandler] Invalid extension request:', request) return } - console.log( - `[HaexHub] ${extension.name} request:`, - request.method, - request.params, - ) - try { let result: unknown @@ -73,17 +85,25 @@ const registerGlobalMessageHandler = () => { throw new Error(`Unknown method: ${request.method}`) } - sourceIframe.contentWindow?.postMessage( + // 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 || '*') + + ;(event.source as Window)?.postMessage( { id: request.id, result, }, - '*', + targetOrigin, ) } catch (error) { - console.error('[HaexHub] Extension request error:', error) + console.error('[ExtensionHandler] Extension request error:', error) - sourceIframe.contentWindow?.postMessage( + // 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 || '*') + + ;(event.source as Window)?.postMessage( { id: request.id, error: { @@ -92,7 +112,7 @@ const registerGlobalMessageHandler = () => { details: error, }, }, - '*', + targetOrigin, ) } }) @@ -153,9 +173,26 @@ export const registerExtensionIFrame = ( } export const unregisterExtensionIFrame = (iframe: HTMLIFrameElement) => { + // Also remove from source registry + const ext = iframeRegistry.get(iframe) + if (ext) { + // Find and remove all sources pointing to this extension + for (const [source, extension] of sourceRegistry.entries()) { + if (extension === ext) { + sourceRegistry.delete(source) + } + } + // Remove from extension-to-window map + extensionToWindowMap.delete(ext.id) + } iframeRegistry.delete(iframe) } +// Export function to get Window for an extension (for broadcasting) +export const getExtensionWindow = (extensionId: string): Window | undefined => { + return extensionToWindowMap.get(extensionId) +} + // ========================================== // Extension Methods // ========================================== @@ -165,10 +202,18 @@ async function handleExtensionMethodAsync( extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr ) { switch (request.method) { - case 'extension.getInfo': - return await invoke('get_extension_info', { + case 'extension.getInfo': { + const info = await invoke('get_extension_info', { extensionId: extension.id, - }) + }) 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" + return { + ...info, + allowedOrigin: window.location.origin, + } + } default: throw new Error(`Unknown extension method: ${request.method}`) } diff --git a/src/layouts/app.vue b/src/layouts/app.vue index fae04c5..54c68fb 100644 --- a/src/layouts/app.vue +++ b/src/layouts/app.vue @@ -28,7 +28,7 @@ diff --git a/src/pages/vault/[vaultId]/extensions/[extensionId].vue b/src/pages/vault/[vaultId]/extensions/[extensionId].vue index 8213d7c..adcf95b 100644 --- a/src/pages/vault/[vaultId]/extensions/[extensionId].vue +++ b/src/pages/vault/[vaultId]/extensions/[extensionId].vue @@ -27,14 +27,34 @@
+ + + + + Console + + {{ visibleLogs.length }} + +
+