How External Plugins Load in GeoLibre Desktop vs Web Build: Security Model Explained
External plugins in GeoLibre load through a unified core system that adapts to each platform—desktop supports ZIP archives and remote URLs via Tauri's filesystem API, while the web build supports only remote URLs with no-op filesystem operations; both enforce a manifest-based trust model where untrusted plugins are quarantined until explicitly approved by the user.
GeoLibre's plugin architecture distinguishes between built-in plugins shipped with the application and external plugins that users add at runtime. Whether you're running the Tauri-based desktop build or the browser-based web build, the core loading logic lives in apps/geolibre-desktop/src/hooks/usePlugins.ts. This article breaks down how external plugin loading differs by platform and how the security model protects users from untrusted code.
Built-In Plugin Loading Foundation
Before examining external plugins, understanding the built-in system provides necessary context.
Built-in plugins reside in the packages/plugins workspace and export from packages/plugins/src/index.ts. The Vite plugin bundled-plugins.ts scans apps/geolibre-desktop/public/plugins/ at build time, generating a virtual module:
import { bundledPluginManifestPaths } from "virtual:bundled-plugins";
The usePlugins.ts hook iterates these manifest URLs, fetches each, converts responses to Blob URLs, and dynamically imports them with import(/* webpackIgnore: true */ blobUrl). Each imported plugin registers with the central PluginManager via manager.registerAll([...]).
This Blob-based dynamic import pattern—critical for isolation—carries over to external plugins on both platforms.
Desktop Build: Full Filesystem and Remote Support
The Tauri-based desktop build offers the complete external plugin feature set. The relevant functions import into usePlugins.ts from apps/geolibre-desktop/src/lib/external-plugins.ts:
import {
installWebPluginArchive,
listInstalledWebPlugins,
loadExternalPlugins,
reloadExternalUrlPlugin,
resolvePluginAssetUrlForLoadedPlugin,
uninstallWebPlugin,
unloadFilesystemPlugin,
unloadRemovedUrlPlugins,
} from "../lib/external-plugins";
ZIP Archive Installation
The installWebPluginArchive function handles desktop-only ZIP installation:
- Extracts
plugin.jsonmanifest from the archive - Validates manifest structure and required fields
- Copies plugin files to the user's data directory via Tauri's filesystem API
- Records the plugin as "installed" in local state
After installation, loadExternalPlugins() reads stored manifests and applies the trust partition.
Trust Partitioning Before Execution
The desktop build implements ../lib/plugin-trust to separate trusted from untrusted plugins before any code executes. The partitionProjectPluginManifestUrls function categorizes manifests based on:
- Previously approved URLs stored in user preferences
- New URLs requiring explicit user review
Only trusted plugins proceed to dynamic import. Untrusted plugins populate a quarantined list visible in the ProjectPluginTrustDialog.tsx UI. Users must explicitly grant trust—untrusted plugins never run.
// Desktop: Installing and loading a plugin from ZIP
await installWebPluginArchive(zipFilePath);
await loadExternalPlugins(); // trust check happens here
Web Build: Remote-Only with Restricted Filesystem
The pure web build uses the identical API surface but with filesystem functions as no-ops. Browser security policies prevent writes to local directories, so:
installWebPluginArchive→ no-opunloadFilesystemPlugin→ no-op- File-based plugin persistence → unavailable
Remote URL loading remains fully functional. The loadExternalPlugins function fetches plugin archives over HTTPS, validates manifests, and imports via Blob URLs—exactly as on desktop. The trust enforcement layer operates identically: untrusted remote plugins remain disabled until user approval through the same ProjectPluginTrustDialog.tsx.
// Web/Desktop: Loading a remote plugin by URL
await reloadExternalUrlPlugin('https://example.com/my-plugin/manifest.json');
// User approves manifest URL in trust dialog
await loadExternalPlugins();
Security Model: Four-Layer Protection
GeoLibre implements a sandboxed plugin trust model with these defenses:
1. Manifest-Based Trust Anchoring
Every external plugin requires a plugin.json manifest. The manifest URL serves as the trust anchor—partitionProjectPluginManifestUrls tracks approval per-URL, not per-plugin-version, preventing substitution attacks.
2. User-Controlled Trust Decisions
No external plugin executes without explicit user approval. The ProjectPluginTrustDialog.tsx presents:
- Manifest origin and requested permissions
- Approve/Revoke actions with persistent storage
Untrusted plugins load in a disabled state with zero code execution.
3. Blob URL Isolation
Dynamic imports use import(/* webpackIgnore: true */ blobUrl) construction. This technique:
- Isolates plugin code from the main application bundle
- Enables runtime-controlled lifecycle (unload via URL revocation)
- Prevents plugins from interfering with core module resolution
4. Defensive Error Handling
The reportPluginError mechanism in usePlugins.ts catches and routes failures to the diagnostics panel. Exceptions during plugin initialization or command execution cannot crash the host application.
Key Files and Responsibilities
| File | Location | Purpose |
|---|---|---|
| usePlugins.ts | apps/geolibre-desktop/src/hooks/ |
Central hook for plugin registration, external loading, and PluginManager access |
| bundled-plugins.ts | apps/geolibre-desktop/vite-plugins/ |
Build-time scan of public/plugins/ generating virtual:bundled-plugins |
| external-plugins.ts | apps/geolibre-desktop/src/lib/ |
Installation, loading, unloading, and trust handling for external plugins |
| plugin-trust.ts | apps/geolibre-desktop/src/lib/ |
Trust partitioning and user decision tracking |
| ProjectPluginTrustDialog.tsx | apps/geolibre-desktop/src/components/layout/ |
UI for approving/rejecting external plugin manifests |
Platform Comparison Summary
| Capability | Desktop (Tauri) | Web Build |
|---|---|---|
| ZIP archive installation | ✓ Full support | ✗ No-op (browser restriction) |
| Remote URL loading | ✓ Full support | ✓ Full support |
| Local filesystem persistence | ✓ Tauri API | ✗ Unavailable |
| Trust dialog enforcement | ✓ Identical | ✓ Identical |
| Blob URL isolation | ✓ Identical | ✓ Identical |
Both builds converge on the same security posture: external code requires explicit user trust and executes in an isolated, revocable sandbox.
Summary
- Desktop builds support full external plugin lifecycle—ZIP installation, filesystem persistence, and remote URLs—via Tauri's native APIs
- Web builds support only remote URL loading with filesystem operations disabled, maintaining API compatibility through no-op stubs
- Security model enforces manifest-based trust with user approval, Blob URL isolation, and defensive error handling across both platforms
- Core implementation lives in
usePlugins.tswith platform-specific behavior delegated toexternal-plugins.tsfunctions - Untrusted plugins are quarantined and never executed, with revocation possible at any time
Frequently Asked Questions
How does GeoLibre prevent malicious plugins from running automatically?
Plugins require a valid plugin.json manifest and explicit user trust approval through ProjectPluginTrustDialog.tsx. The partitionProjectPluginManifestUrls function in plugin-trust.ts separates untrusted manifests before any dynamic import occurs. Untrusted plugins remain in a disabled state with no code execution path.
Can web users install plugins from ZIP files like desktop users?
No. Browser security policies prevent local filesystem writes, so installWebPluginArchive and related filesystem functions are no-ops in the web build. Web users can only load plugins from remote HTTPS URLs that pass the same trust verification process.
What happens if a trusted plugin becomes untrusted or is revoked?
The PluginManager tracks loaded plugins by their Blob URLs. When trust is revoked, unloadRemovedUrlPlugins or direct URL revocation removes the plugin from the active set. The plugin's code is no longer reachable through its Blob URL, and subsequent loadExternalPlugins calls exclude it from registration.
Where is the plugin trust state persisted across sessions?
User trust decisions for manifest URLs are stored persistently and retrieved by partitionProjectPluginManifestUrls during each loadExternalPlugins invocation. The exact storage mechanism (Tauri store for desktop, browser storage for web) is abstracted by the trust layer, with the same approval UI presented regardless of platform.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →