extend extensions implementation

This commit is contained in:
2025-09-30 16:16:33 +02:00
parent f1daa6b576
commit 56e75977cd
8 changed files with 540 additions and 11 deletions

View File

@ -28,6 +28,72 @@ pub struct ExtensionManifest {
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionInfoResponse {
pub key_hash: String,
pub name: String,
pub full_id: String,
pub version: String,
pub display_name: Option<String>,
pub namespace: Option<String>,
pub allowed_origin: String,
}
impl ExtensionInfoResponse {
pub fn from_extension(extension: &Extension) -> Self {
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
let allowed_origin = get_tauri_origin();
Self {
key_hash: calculate_key_hash(&extension.manifest.id),
name: extension.manifest.name.clone(),
full_id: format!(
"{}/{}@{}",
calculate_key_hash(&extension.manifest.id),
extension.manifest.name,
extension.manifest.version
),
version: extension.manifest.version.clone(),
display_name: Some(extension.manifest.name.clone()),
namespace: extension.manifest.author.clone(),
allowed_origin,
}
}
}
fn get_tauri_origin() -> String {
#[cfg(target_os = "windows")]
{
"https://tauri.localhost".to_string()
}
#[cfg(target_os = "macos")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "linux")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "android")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "ios")]
{
"tauri://localhost".to_string()
}
}
// Dummy-Funktion für Key Hash (du implementierst das richtig mit SHA-256)
fn calculate_key_hash(id: &str) -> String {
// TODO: Implementiere SHA-256 Hash vom Public Key
// Für jetzt nur Placeholder
format!("{:0<20}", id.chars().take(20).collect::<String>())
}
/// Extension source type (production vs development)
#[derive(Debug, Clone)]
pub enum ExtensionSource {

View File

@ -16,7 +16,6 @@ pub struct DbExtensionPermission {
pub extension_id: String,
pub resource: String,
pub operation: String,
pub path: String,
}
/// Prüft Leseberechtigungen für eine Extension
@ -168,7 +167,7 @@ async fn check_table_permissions(
for table_name in table_names {
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(table_name));
.any(|perm| perm.resource.contains(table_name));
if !has_permission {
return Err(ExtensionError::permission_denied(
@ -207,7 +206,6 @@ pub async fn get_extension_permissions(
extension_id: row.get(1)?,
resource: row.get(2)?,
operation: row.get(3)?,
path: row.get(4)?,
})
})
.map_err(|e| DatabaseError::QueryError {

View File

@ -1,5 +1,19 @@
use crate::extension::core::{ExtensionInfoResponse, ExtensionManager};
use tauri::State;
pub mod core;
pub mod database;
pub mod error;
pub mod filesystem;
pub mod permission_manager;
#[tauri::command]
pub fn get_extension_info(
extension_id: String,
extension_manager: State<ExtensionManager>,
) -> Result<ExtensionInfoResponse, String> {
let extension = extension_manager
.get_extension(&extension_id)
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
Ok(ExtensionInfoResponse::from_extension(&extension))
}

View File

@ -52,7 +52,7 @@ impl PermissionManager {
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(table_name));
.any(|perm| perm.resource.contains(table_name));
if !has_permission {
return Err(ExtensionError::permission_denied(

View File

@ -0,0 +1,52 @@
/**
* Broadcasts context changes to all active extensions
*/
export const useExtensionContextBroadcast = () => {
const extensionIframes = ref<HTMLIFrameElement[]>([])
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
extensionIframes.value.push(iframe)
}
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe)
}
const broadcastContextChange = (context: {
theme: string
locale: string
platform: string
}) => {
const message = {
type: 'context.changed',
data: { context },
timestamp: Date.now(),
}
extensionIframes.value.forEach((iframe) => {
iframe.contentWindow?.postMessage(message, '*')
})
}
const broadcastSearchRequest = (query: string, requestId: string) => {
const message = {
type: 'search.request',
data: {
query: { query, limit: 10 },
requestId,
},
timestamp: Date.now(),
}
extensionIframes.value.forEach((iframe) => {
iframe.contentWindow?.postMessage(message, '*')
})
}
return {
registerExtensionIframe,
unregisterExtensionIframe,
broadcastContextChange,
broadcastSearchRequest,
}
}

View File

@ -0,0 +1,358 @@
import type { IHaexHubExtensionLink } from '~/types/haexhub'
interface ExtensionRequest {
id: string
method: string
params: Record<string, unknown>
timestamp: number
}
interface ExtensionResponse {
id: string
result?: unknown
error?: {
code: string
message: string
details?: unknown
}
}
export const useExtensionMessageHandler = (
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) => {
const handleMessage = async (event: MessageEvent) => {
// Security: Only accept messages from our iframe
if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) {
return
}
const request = event.data as ExtensionRequest
// Validate request structure
if (!request.id || !request.method) {
console.error('Invalid extension request:', request)
return
}
console.log('[HaexHub] Extension request:', request.method, request.params)
try {
let result: unknown
// Route request to appropriate handler
if (request.method.startsWith('extension.')) {
result = await handleExtensionMethod(request, extension)
} else if (request.method.startsWith('db.')) {
result = await handleDatabaseMethod(request, extension)
} else if (request.method.startsWith('permissions.')) {
result = await handlePermissionsMethod(request, extension)
} else if (request.method.startsWith('context.')) {
result = await handleContextMethod(request)
} else if (request.method.startsWith('search.')) {
result = await handleSearchMethod(request, extension)
} else {
throw new Error(`Unknown method: ${request.method}`)
}
// Send success response
sendResponse(iframeRef.value, {
id: request.id,
result,
})
} catch (error) {
console.error('[HaexHub] Extension request error:', error)
// Send error response
sendResponse(iframeRef.value, {
id: request.id,
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : 'Unknown error',
details: error,
},
})
}
}
const sendResponse = (
iframe: HTMLIFrameElement,
response: ExtensionResponse,
) => {
iframe.contentWindow?.postMessage(response, '*')
}
// Register/unregister message listener
onMounted(() => {
window.addEventListener('message', handleMessage)
})
onUnmounted(() => {
window.removeEventListener('message', handleMessage)
})
return {
handleMessage,
}
}
// ==========================================
// Extension Methods
// ==========================================
async function handleExtensionMethod(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) {
switch (request.method) {
case 'extension.getInfo':
return {
keyHash: extension.value?.id || '', // TODO: Real key hash
name: extension.value?.name || '',
fullId: `${extension.value?.id}/${extension.value?.name}@${extension.value?.version}`,
version: extension.value?.version || '',
displayName: extension.value?.name,
namespace: extension.value?.author,
allowedOrigin: window.location.origin, // "tauri://localhost"
}
case 'extensions.getDependencies':
// TODO: Implement dependencies from manifest
return []
default:
throw new Error(`Unknown extension method: ${request.method}`)
}
}
// ==========================================
// Database Methods
// ==========================================
async function handleDatabaseMethod(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) {
const { currentVault } = useVaultStore()
if (!currentVault) {
throw new Error('No vault available')
}
if (!extension.value) {
throw new Error('Extension not found')
}
const params = request.params as { query?: string; params?: unknown[] }
switch (request.method) {
case 'db.query': {
// Validate permission
await validateDatabaseAccess(extension.value, params.query || '', 'read')
// Execute query
const result = await currentVault.drizzle.execute(params.query || '')
return {
rows: result.rows || [],
rowsAffected: 0,
lastInsertId: undefined,
}
}
case 'db.execute': {
// Validate permission
await validateDatabaseAccess(extension.value, params.query || '', 'write')
// Execute query
const result = await currentVault.drizzle.execute(params.query || '')
return {
rows: [],
rowsAffected: result.rowsAffected || 0,
lastInsertId: result.lastInsertId,
}
}
case 'db.transaction': {
const statements =
(request.params as { statements?: string[] }).statements || []
// Validate all statements
for (const stmt of statements) {
await validateDatabaseAccess(extension.value, stmt, 'write')
}
// Execute transaction
await currentVault.drizzle.transaction(async (tx) => {
for (const stmt of statements) {
await tx.execute(stmt)
}
})
return { success: true }
}
default:
throw new Error(`Unknown database method: ${request.method}`)
}
}
// ==========================================
// Permission Validation
// ==========================================
async function validateDatabaseAccess(
extension: IHaexHubExtensionLink,
query: string,
operation: 'read' | 'write',
): Promise<void> {
// Extract table name from query
const tableMatch = query.match(/(?:FROM|INTO|UPDATE|TABLE)\s+(\w+)/i)
if (!tableMatch) {
throw new Error('Could not extract table name from query')
}
const tableName = tableMatch[1]
// Check if it's the extension's own table
const extensionPrefix = `${extension.id}_${extension.name?.replace(/-/g, '_')}_`
const isOwnTable = tableName.startsWith(extensionPrefix)
if (isOwnTable) {
// Own tables: always allowed
return
}
// External table: Check permissions
const hasPermission = await checkDatabasePermission(
extension.id,
tableName,
operation,
)
if (!hasPermission) {
throw new Error(`Permission denied: ${operation} access to ${tableName}`)
}
}
async function checkDatabasePermission(
extensionId: string,
tableName: string,
operation: 'read' | 'write',
): Promise<boolean> {
// TODO: Query permissions from database
// SELECT * FROM db_extension_permissions
// WHERE extension_id = ? AND resource = ? AND operation = ?
console.warn('TODO: Implement permission check', {
extensionId,
tableName,
operation,
})
// For now: deny by default
return false
}
// ==========================================
// Permission Methods
// ==========================================
async function handlePermissionsMethod(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) {
switch (request.method) {
case 'permissions.database.request': {
const params = request.params as {
resource: string
operation: 'read' | 'write'
reason?: string
}
// TODO: Show user dialog to grant/deny permission
console.log('[HaexHub] Permission request:', params)
// For now: return ASK
return {
status: 'ask',
permanent: false,
}
}
case 'permissions.database.check': {
const params = request.params as {
resource: string
operation: 'read' | 'write'
}
const hasPermission = await checkDatabasePermission(
extension.value?.id || '',
params.resource,
params.operation,
)
return {
status: hasPermission ? 'granted' : 'denied',
permanent: true,
}
}
default:
throw new Error(`Unknown permission method: ${request.method}`)
}
}
// ==========================================
// Context Methods
// ==========================================
async function handleContextMethod(request: ExtensionRequest) {
const { theme } = useThemeStore()
const { locale } = useI18n()
switch (request.method) {
case 'context.get':
return {
theme: theme.value || 'system',
locale: locale.value,
platform: detectPlatform(),
}
default:
throw new Error(`Unknown context method: ${request.method}`)
}
}
function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
const width = window.innerWidth
if (width < 768) return 'mobile'
if (width < 1024) return 'tablet'
return 'desktop'
}
// ==========================================
// Search Methods
// ==========================================
async function handleSearchMethod(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) {
switch (request.method) {
case 'search.respond': {
const params = request.params as {
requestId: string
results: unknown[]
}
// TODO: Store search results for display
console.log('[HaexHub] Search results from extension:', params)
return { success: true }
}
default:
throw new Error(`Unknown search method: ${request.method}`)
}
}

View File

@ -1,29 +1,68 @@
<template>
<div class="w-full h-full overflow-scroll">
<div>
{{ iframeIndex }}
<div
v-if="!iFrameSrc"
class="flex items-center justify-center h-full"
>
<p>{{ t('loading') }}</p>
</div>
<iframe
v-if="iframeIndex"
v-else
ref="iFrameRef"
class="w-full h-full"
:src="iframeIndex"
sandbox="allow-scripts allow-same-origin"
:src="iFrameSrc"
sandbox="allow-scripts "
allow="autoplay; speaker-selection; encrypted-media;"
/>
</div>
</template>
<script setup lang="ts">
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
definePageMeta({
name: 'haexExtension',
})
const { extensionEntry: iframeSrc } = storeToRefs(useExtensionsStore())
const { t } = useI18n()
const iframeIndex = computed(() =>
const iFrameRef = useTemplateRef('iFrameRef')
const { extensionEntry: iframeSrc, currentExtension } =
storeToRefs(useExtensionsStore())
const iFrameSrc = computed(() =>
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
)
useExtensionMessageHandler(iFrameRef, currentExtension)
const { currentTheme } = storeToRefs(useUiStore())
const { locale } = useI18n()
watch([currentTheme, locale], () => {
if (iFrameRef.value?.contentWindow) {
iFrameRef.value.contentWindow.postMessage(
{
type: 'context.changed',
data: {
context: {
theme: currentTheme.value || 'system',
locale: locale.value,
platform:
window.innerWidth < 768
? 'mobile'
: window.innerWidth < 1024
? 'tablet'
: 'desktop',
},
},
timestamp: Date.now(),
},
'*',
)
}
})
</script>
<i18n lang="yaml">

View File

@ -1,3 +1,5 @@
// src/stores/vault/index.ts
import { drizzle } from 'drizzle-orm/sqlite-proxy'
import { invoke } from '@tauri-apps/api/core'
import * as schema from '@/../src-tauri/database/schemas/vault'