From 5d6acfef9383a52ad0f09bb1da38ac4ec1a1ed20 Mon Sep 17 00:00:00 2001
From: haex
Date: Sat, 11 Oct 2025 20:42:13 +0200
Subject: [PATCH] extensions fixed
---
.gitignore | 3 +-
src-tauri/bindings/ExtensionInfoResponse.ts | 2 +-
src-tauri/src/extension/core/manifest.rs | 14 +-
src-tauri/src/extension/core/protocol.rs | 269 ------------------
src-tauri/src/extension/core/types.rs | 4 +-
src/components/haex/extension/launcher.vue | 123 ++++++++
.../haex/extension/marketplace-card.vue | 9 +-
src/components/haex/menu/applications.vue | 58 ----
src/composables/extensionContextBroadcast.ts | 33 ++-
src/composables/extensionMessageHandler.ts | 97 +++++--
src/layouts/app.vue | 2 +-
.../[vaultId]/extensions/[extensionId].vue | 186 +++++++++++-
.../vault/[vaultId]/extensions/index.vue | 113 ++------
src/plugins/console-interceptor.ts | 77 +++++
src/stores/extensions/index.ts | 129 +--------
src/stores/extensions/tabs.ts | 34 ++-
src/types/haexhub.d.ts | 23 +-
17 files changed, 582 insertions(+), 594 deletions(-)
create mode 100644 src/components/haex/extension/launcher.vue
delete mode 100644 src/components/haex/menu/applications.vue
create mode 100644 src/plugins/console-interceptor.ts
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 }}
+
+
+
+
+
+
+
+
Console Output
+
+ Clear
+
+
+
+
+
+
+
+ Showing last {{ maxVisibleLogs }} of {{ consoleLogs.length }} logs
+
+
+
+
+
+
+
+
+ [{{ log.timestamp }}] [{{ log.level.toUpperCase() }}]
+
+
+
+
+
{{ log.message }}
+
+
+
+
+ No console messages yet
+
+
+
+
+>
+
+// Only show last N logs for performance
+const visibleLogs = computed(() => {
+ return consoleLogs.value.slice(-maxVisibleLogs.value)
+})
+
// Extension aus Route öffnen
//const extensionId = computed(() => route.params.extensionId as string)
@@ -94,19 +209,71 @@ const dummyIframeRef = ref(null)
const dummyExtensionRef = computed(() => null)
useExtensionMessageHandler(dummyIframeRef, dummyExtensionRef)
+// Track which iframes have been registered to prevent duplicate registrations
+const registeredIFrames = new WeakSet()
+
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
if (!el) return
+ // Prevent duplicate registration (Vue calls ref functions on every render)
+ if (registeredIFrames.has(el)) {
+ return
+ }
+
+ console.log('[Vue Debug] ========== registerIFrame called ==========')
+ console.log('[Vue Debug] Extension ID:', extensionId)
+ console.log('[Vue Debug] Element:', 'HTMLIFrameElement')
+
+ // Mark as registered
+ registeredIFrames.add(el)
+
// Registriere IFrame im Store
tabsStore.registerIFrame(extensionId, el)
// Registriere IFrame im globalen Message Handler Registry
const tab = tabsStore.openTabs.get(extensionId)
if (tab?.extension) {
+ console.log('[Vue Debug] Registering iframe in message handler for:', tab.extension.name)
registerExtensionIFrame(el, tab.extension)
+ console.log('[Vue Debug] Registration complete!')
+ } else {
+ console.error('[Vue Debug] ❌ No tab found for extension ID:', extensionId)
+ }
+ console.log('[Vue Debug] ========================================')
+}
+
+// Listen for console messages from extensions (via postMessage)
+const handleExtensionConsole = (event: MessageEvent) => {
+ if (event.data?.type === 'console.forward') {
+ const { timestamp, level, message } = event.data.data
+ consoleLogs.value.push({
+ timestamp,
+ level,
+ message: `[Extension] ${message}`,
+ })
+
+ // Limit to last 1000 logs
+ if (consoleLogs.value.length > 1000) {
+ consoleLogs.value = consoleLogs.value.slice(-1000)
+ }
}
}
+onMounted(() => {
+ window.addEventListener('message', handleExtensionConsole)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('message', handleExtensionConsole)
+
+ // Unregister all iframes when the page unmounts
+ tabsStore.openTabs.forEach((tab) => {
+ if (tab.iframe) {
+ unregisterExtensionIFrame(tab.iframe)
+ }
+ })
+})
+
// Cleanup wenn Tabs geschlossen werden
watch(
() => tabsStore.openTabs,
@@ -184,11 +351,16 @@ watch([currentTheme, locale], () => {
})
})
-// Cleanup beim Verlassen
-onBeforeUnmount(() => {
- // Optional: Alle Tabs schließen oder offen lassen
- // tabsStore.closeAllTabs()
-})
+// Copy to clipboard function
+const copyToClipboard = async (text: string) => {
+ try {
+ await navigator.clipboard.writeText(text)
+ // Optional: Show success toast
+ console.log('Copied to clipboard')
+ } catch (err) {
+ console.error('Failed to copy:', err)
+ }
+}
diff --git a/src/pages/vault/[vaultId]/extensions/index.vue b/src/pages/vault/[vaultId]/extensions/index.vue
index c92c2b5..e746fea 100644
--- a/src/pages/vault/[vaultId]/extensions/index.vue
+++ b/src/pages/vault/[vaultId]/extensions/index.vue
@@ -67,27 +67,15 @@
v-if="filteredExtensions.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
-
+
-
-
-
-
-
+ :extension="ext"
+ :is-installed="ext.isInstalled"
+ @install="onInstallFromMarketplace(ext)"
+ @details="onShowExtensionDetails(ext)"
+ />
@@ -132,6 +120,7 @@
import type {
IHaexHubExtension,
IHaexHubExtensionManifest,
+ IMarketplaceExtension,
} from '~/types/haexhub'
import { open } from '@tauri-apps/plugin-dialog'
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
@@ -208,7 +197,7 @@ const categories = computed(() => [
])
// Dummy Marketplace Extensions (später von API laden)
-const marketplaceExtensions = ref([
+const marketplaceExtensions = ref
([
{
id: 'haex-passy',
name: 'HaexPassDummy',
@@ -217,12 +206,14 @@ const marketplaceExtensions = ref([
description:
'Sicherer Passwort-Manager mit Ende-zu-Ende-Verschlüsselung und Autofill-Funktion.',
icon: 'i-heroicons-lock-closed',
+ homepage: null,
downloads: 15420,
rating: 4.8,
verified: true,
tags: ['security', 'password', 'productivity'],
category: 'security',
downloadUrl: '/extensions/haex-pass-1.0.0.haextension',
+ isInstalled: false,
},
{
id: 'haex-notes',
@@ -232,12 +223,14 @@ const marketplaceExtensions = ref([
description:
'Markdown-basierter Notizen-Editor mit Syntax-Highlighting und Live-Preview.',
icon: 'i-heroicons-document-text',
+ homepage: null,
downloads: 8930,
rating: 4.5,
verified: true,
tags: ['productivity', 'notes', 'markdown'],
category: 'productivity',
downloadUrl: '/extensions/haex-notes-2.1.0.haextension',
+ isInstalled: false,
},
{
id: 'haex-backup',
@@ -247,12 +240,14 @@ const marketplaceExtensions = ref([
description:
'Automatische Backups deiner Daten mit Cloud-Sync-Unterstützung.',
icon: 'i-heroicons-cloud-arrow-up',
+ homepage: null,
downloads: 5240,
rating: 4.6,
verified: false,
tags: ['backup', 'cloud', 'utilities'],
category: 'utilities',
downloadUrl: '/extensions/haex-backup-1.5.2.haextension',
+ isInstalled: false,
},
{
id: 'haex-calendar',
@@ -262,12 +257,14 @@ const marketplaceExtensions = ref([
description:
'Integrierter Kalender mit Event-Management und Synchronisation.',
icon: 'i-heroicons-calendar',
+ homepage: null,
downloads: 12100,
rating: 4.7,
verified: true,
tags: ['productivity', 'calendar', 'events'],
category: 'productivity',
downloadUrl: '/extensions/haex-calendar-3.0.1.haextension',
+ isInstalled: false,
},
{
id: 'haex-2fa',
@@ -277,12 +274,14 @@ const marketplaceExtensions = ref([
description:
'2-Faktor-Authentifizierung Manager mit TOTP und Backup-Codes.',
icon: 'i-heroicons-shield-check',
+ homepage: null,
downloads: 7800,
rating: 4.9,
verified: true,
tags: ['security', '2fa', 'authentication'],
category: 'security',
downloadUrl: '/extensions/haex-2fa-1.2.0.haextension',
+ isInstalled: false,
},
{
id: 'haex-github',
@@ -292,39 +291,26 @@ const marketplaceExtensions = ref([
description:
'Direkter Zugriff auf GitHub Repositories, Issues und Pull Requests.',
icon: 'i-heroicons-code-bracket',
+ homepage: null,
downloads: 4120,
rating: 4.3,
verified: false,
tags: ['integration', 'github', 'development'],
category: 'integration',
downloadUrl: '/extensions/haex-github-1.0.5.haextension',
+ isInstalled: false,
},
])
-// Combine installed extensions with marketplace extensions
-const allExtensions = computed(() => {
- // Map installed extensions to marketplace format
- const installed = extensionStore.availableExtensions.map((ext) => ({
- id: ext.id,
- name: ext.name,
- version: ext.version,
- author: ext.author || 'Unknown',
- description: 'Installed Extension',
- icon: ext.icon || 'i-heroicons-puzzle-piece',
- downloads: 0,
- rating: 0,
- verified: false,
- tags: [],
- category: 'utilities',
- downloadUrl: '',
- isInstalled: true,
+// Mark marketplace extensions as installed if they exist in availableExtensions
+const allExtensions = computed((): IMarketplaceExtension[] => {
+ return marketplaceExtensions.value.map((ext) => ({
+ ...ext,
+ // Check if this marketplace extension is already installed
+ isInstalled: extensionStore.availableExtensions.some(
+ (installed) => installed.name === ext.name,
+ ),
}))
-
- console.log('Installed extensions count:', installed.length)
- console.log('All extensions:', [...installed, ...marketplaceExtensions.value])
-
- // Merge with marketplace extensions
- return [...installed, ...marketplaceExtensions.value]
})
// Filtered Extensions
@@ -333,7 +319,7 @@ const filteredExtensions = computed(() => {
const matchesSearch =
!searchQuery.value ||
ext.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
- ext.description.toLowerCase().includes(searchQuery.value.toLowerCase())
+ ext.description?.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesCategory =
selectedCategory.value === 'all' ||
@@ -343,14 +329,6 @@ const filteredExtensions = computed(() => {
})
})
-// Check if extension is installed
-const isExtensionInstalled = (extensionId: string) => {
- return (
- extensionStore.availableExtensions.some((ext) => ext.id === extensionId) ||
- allExtensions.value.some((ext) => ext.id === extensionId)
- )
-}
-
// Install from marketplace
const onInstallFromMarketplace = async (ext: unknown) => {
console.log('Install from marketplace:', ext)
@@ -364,35 +342,6 @@ const onShowExtensionDetails = (ext: unknown) => {
// TODO: Show extension details modal
}
-// Navigate to installed extension
-const router = useRouter()
-const route = useRoute()
-const localePath = useLocalePath()
-
-const navigateToExtension = (extensionId: string) => {
- router.push(
- localePath({
- name: 'haexExtension',
- params: {
- vaultId: route.params.vaultId,
- extensionId,
- },
- }),
- )
-}
-
-// Show extension settings
-const onShowExtensionSettings = (ext: unknown) => {
- console.log('Show settings:', ext)
- // TODO: Show extension settings modal
-}
-
-// Show remove dialog
-const onShowRemoveDialog = (ext: any) => {
- extensionToBeRemoved.value = ext
- showRemoveDialog.value = true
-}
-
const onSelectExtensionAsync = async () => {
try {
extension.path = await open({ directory: false, recursive: true })
@@ -405,7 +354,7 @@ const onSelectExtensionAsync = async () => {
// Check if already installed using full_extension_id
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
const isAlreadyInstalled = extensionStore.availableExtensions.some(
- ext => ext.id === fullExtensionId
+ (ext) => ext.id === fullExtensionId,
)
if (isAlreadyInstalled) {
diff --git a/src/plugins/console-interceptor.ts b/src/plugins/console-interceptor.ts
new file mode 100644
index 0000000..0874886
--- /dev/null
+++ b/src/plugins/console-interceptor.ts
@@ -0,0 +1,77 @@
+/**
+ * Global Console Interceptor Plugin
+ * Captures all console messages app-wide for debugging
+ */
+
+export interface ConsoleLog {
+ timestamp: string
+ level: 'log' | 'info' | 'warn' | 'error' | 'debug'
+ message: string
+}
+
+// Global storage for console logs
+export const globalConsoleLogs = ref([])
+
+// Store original console methods
+const originalConsole = {
+ log: console.log,
+ info: console.info,
+ warn: console.warn,
+ error: console.error,
+ debug: console.debug,
+}
+
+function interceptConsole(level: 'log' | 'info' | 'warn' | 'error' | 'debug') {
+ console[level] = function (...args: unknown[]) {
+ // Call original console method
+ originalConsole[level].apply(console, args)
+
+ // Add to global log display
+ const timestamp = new Date().toLocaleTimeString()
+ const message = args
+ .map((arg) => {
+ if (arg === null) return 'null'
+ if (arg === undefined) return 'undefined'
+ if (typeof arg === 'object') {
+ try {
+ return JSON.stringify(arg, null, 2)
+ } catch {
+ return String(arg)
+ }
+ }
+ return String(arg)
+ })
+ .join(' ')
+
+ globalConsoleLogs.value.push({
+ timestamp,
+ level,
+ message,
+ })
+
+ // Limit to last 1000 logs
+ if (globalConsoleLogs.value.length > 1000) {
+ globalConsoleLogs.value = globalConsoleLogs.value.slice(-1000)
+ }
+ }
+}
+
+export default defineNuxtPlugin(() => {
+ // Install console interceptors immediately
+ interceptConsole('log')
+ interceptConsole('info')
+ interceptConsole('warn')
+ interceptConsole('error')
+ interceptConsole('debug')
+
+ console.log('[HaexHub] Global console interceptor installed')
+
+ return {
+ provide: {
+ consoleLogs: globalConsoleLogs,
+ clearConsoleLogs: () => {
+ globalConsoleLogs.value = []
+ },
+ },
+ }
+})
diff --git a/src/stores/extensions/index.ts b/src/stores/extensions/index.ts
index 3f487bb..89a88a8 100644
--- a/src/stores/extensions/index.ts
+++ b/src/stores/extensions/index.ts
@@ -8,16 +8,7 @@ import type {
} from '~/types/haexhub'
import type { ExtensionPreview } from '@bindings/ExtensionPreview'
import type { ExtensionPermissions } from '~~/src-tauri/bindings/ExtensionPermissions'
-
-interface ExtensionInfoResponse {
- keyHash: string
- name: string
- fullId: string
- version: string
- displayName: string | null
- namespace: string | null
- allowedOrigin: string
-}
+import type { ExtensionInfoResponse } from '~~/src-tauri/bindings/ExtensionInfoResponse'
/* const manifestFileName = 'manifest.json'
const logoFileName = 'icon.svg' */
@@ -130,8 +121,10 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
name: ext.displayName || ext.name,
version: ext.version,
author: ext.namespace,
- icon: null,
- enabled: true,
+ icon: ext.icon,
+ enabled: ext.enabled,
+ description: ext.description,
+ homepage: ext.homepage,
}))
} catch (error) {
console.error('Fehler beim Laden der Extensions:', error)
@@ -192,54 +185,6 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
}
}
- /* const installAsync = async (extensionDirectory: string | null) => {
- try {
- if (!extensionDirectory)
- throw new Error('Kein Ordner für Erweiterung angegeben')
- const manifestPath = await join(extensionDirectory, manifestFileName)
- const manifest = (await JSON.parse(
- await readTextFile(manifestPath),
- )) as IHaexHubExtensionManifest
-
- const destination = await getExtensionPathAsync(
- manifest.id,
- manifest.version,
- )
-
- await checkSourceExtensionDirectoryAsync(extensionDirectory)
-
- await invoke('copy_directory', {
- source: extensionDirectory,
- destination,
- })
-
- const logoFilePath = await join(destination, logoFileName)
- const logo = await readTextFile(logoFilePath)
-
- const { currentVault } = storeToRefs(useVaultStore())
- const res = await currentVault.value?.drizzle
- .insert(haexExtensions)
- .values({
- id: manifest.id,
- name: manifest.name,
- author: manifest.author,
- enabled: true,
- url: manifest.url,
- version: manifest.version,
- icon: logo,
- })
-
- console.log('insert extensions', res)
- addNotificationAsync({
- type: 'success',
- text: `${manifest.name} wurde installiert`,
- })
- } catch (error) {
- addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
- throw error
- }
- } */
-
const removeExtensionAsync = async (extensionId: string, version: string) => {
try {
await invoke('remove_extension', {
@@ -356,70 +301,6 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
})
return preview.value
}
- /* const readManifestFileAsync = async (
- extensionId: string,
- version: string,
- ) => {
- try {
- if (!(await isExtensionInstalledAsync({ id: extensionId, version })))
- return null
-
- const extensionPath = await getExtensionPathAsync(
- extensionId,
- `${version}`,
- )
- const manifestPath = await join(extensionPath, manifestFileName)
- const manifest = (await JSON.parse(
- await readTextFile(manifestPath),
- )) as IHaexHubExtensionManifest
-
- return manifest
- } catch (error) {
- addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
- console.error('ERROR readManifestFileAsync', error)
- }
- } */
-
- /* const extensionEntry = computedAsync(
- async () => {
- try {
-
-
- if (!currentExtension.value?.id || !currentExtension.value.version) {
- console.log('extension id or entry missing', currentExtension.value)
- return '' // "no mani: " + currentExtension.value;
- }
-
- const extensionPath = await getExtensionPathAsync(
- currentExtension.value?.id,
- currentExtension.value?.version,
- ) //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
-
- console.log('extensionEntry extensionPath', extensionPath)
- const manifest = await readManifestFileAsync(
- currentExtension.value.id,
- currentExtension.value.version,
- )
-
- if (!manifest) return '' //"no manifest readable";
-
- //const entryPath = await join(extensionPath, manifest.entry)
-
- const hexName = stringToHex(
- JSON.stringify({
- id: currentExtension.value.id,
- version: currentExtension.value.version,
- }),
- )
-
- return `haex-extension://${hexName}`
- } catch (error) {
- console.error('ERROR extensionEntry', error)
- }
- },
- null,
- { lazy: true },
- ) */
return {
availableExtensions,
diff --git a/src/stores/extensions/tabs.ts b/src/stores/extensions/tabs.ts
index 97fcc0e..01f41dc 100644
--- a/src/stores/extensions/tabs.ts
+++ b/src/stores/extensions/tabs.ts
@@ -1,5 +1,6 @@
// stores/extensions/tabs.ts
import type { IHaexHubExtension } from '~/types/haexhub'
+import { getExtensionWindow } from '~/composables/extensionMessageHandler'
interface ExtensionTab {
extension: IHaexHubExtension
@@ -40,6 +41,12 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
return
}
+ // Check if extension is enabled
+ if (!extension.enabled) {
+ console.warn(`Extension ${extensionId} ist deaktiviert und kann nicht geöffnet werden`)
+ return
+ }
+
// Bereits geöffnet? Nur aktivieren
if (openTabs.value.has(extensionId)) {
setActiveTab(extensionId)
@@ -78,8 +85,25 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
// Zeige neuen Tab
const newTab = openTabs.value.get(extensionId)
if (newTab) {
+ const now = Date.now()
+ const inactiveDuration = now - newTab.lastAccessed
+ const TEN_MINUTES = 10 * 60 * 1000
+
+ // Reload iframe if inactive for more than 10 minutes
+ if (inactiveDuration > TEN_MINUTES && newTab.iframe) {
+ console.log(`[TabStore] Reloading extension ${extensionId} after ${Math.round(inactiveDuration / 1000)}s inactivity`)
+ const currentSrc = newTab.iframe.src
+ newTab.iframe.src = 'about:blank'
+ // Small delay to ensure reload
+ setTimeout(() => {
+ if (newTab.iframe) {
+ newTab.iframe.src = currentSrc
+ }
+ }, 50)
+ }
+
newTab.isVisible = true
- newTab.lastAccessed = Date.now()
+ newTab.lastAccessed = now
activeTabId.value = extensionId
}
}
@@ -113,8 +137,12 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
}
const broadcastToAllTabs = (message: unknown) => {
- openTabs.value.forEach(({ iframe }) => {
- iframe?.contentWindow?.postMessage(message, '*')
+ openTabs.value.forEach(({ extension }) => {
+ // Use sandbox-compatible window reference
+ const win = getExtensionWindow(extension.id)
+ if (win) {
+ win.postMessage(message, '*')
+ }
})
}
diff --git a/src/types/haexhub.d.ts b/src/types/haexhub.d.ts
index bc80b82..53ac8af 100644
--- a/src/types/haexhub.d.ts
+++ b/src/types/haexhub.d.ts
@@ -20,10 +20,9 @@ export interface IHaexHubExtensionManifest {
}
}
-export interface IHaexHubExtensionLink extends IHaexHubExtension {
- installed: boolean
-}
-
+/**
+ * Installed extension from database/backend
+ */
export interface IHaexHubExtension {
id: string
name: string
@@ -31,4 +30,20 @@ export interface IHaexHubExtension {
author: string | null
icon: string | null
enabled: boolean
+ description: string | null
+ homepage: string | null
+}
+
+/**
+ * Marketplace extension with additional metadata
+ * Extends IHaexHubExtension with marketplace-specific fields
+ */
+export interface IMarketplaceExtension extends Omit {
+ downloads: number
+ rating: number
+ verified: boolean
+ tags: string[]
+ category: string
+ downloadUrl: string
+ isInstalled: boolean
}