Skip to content

Architecture

Finnegan's Owner edited this page Jun 29, 2026 · 4 revisions

Architecture

Launcher is a Tauri v2 desktop application with a Rust backend and a Nuxt 4 / Vue 3 frontend.

Project Structure

launcher/
├── app/                          # Frontend (Nuxt 4)
│   ├── app.vue                   # Root: renders the main UI, or the console
│   │                             #   when running in a console-* webview window
│   ├── assets/css/main.css       # Tailwind CSS v4 design tokens
│   ├── components/               # Vue components
│   │   ├── BriefServerInfo.vue   # Server row in the list
│   │   ├── ConnectionInput.vue   # Reusable form input
│   │   ├── ConsoleLog.vue        # Native console window UI (stdout/stderr)
│   │   ├── InsertableDropdown.vue # Dropdown with inline creation
│   │   └── TrustCertModal.vue    # Certificate trust / cert-change dialog
│   ├── composables/              # Vue composables
│   │   ├── useConfirmRejectModal.ts # Imperative modal mounting
│   │   ├── useConsole.ts         # Subscribes a console window to its stream
│   │   ├── useConsoleTheme.ts    # Per-console light/dark toggle
│   │   ├── useTheme.ts           # Main-window theme
│   │   └── useZoom.ts            # Keyboard zoom (Cmd/Ctrl +/-/0)
│   ├── enums.ts                  # Shared frontend enums
│   ├── pages/                    # File-based routing
│   │   ├── index.vue             # Main server list
│   │   └── connections/[id].vue  # Connection editor
│   └── types.d.ts                # TypeScript interfaces
├── src-tauri/                    # Backend (Rust)
│   ├── src/
│   │   ├── main.rs               # Tauri commands and app setup
│   │   ├── connection.rs         # ConnectionStore, persistence, cert pins
│   │   ├── webstart.rs           # JNLP parsing, JAR downloading, Java process
│   │   ├── tls.rs                # Per-connection TLS cert pinning (TOFU)
│   │   └── console.rs            # Native console: streams stdout/stderr
│   ├── capabilities/
│   │   ├── default.json          # Main-window permissions
│   │   └── console.json          # Console-window permissions (console-*)
│   ├── Cargo.toml                # Rust dependencies
│   └── tauri.conf.json           # Tauri app configuration
└── nuxt.config.ts                # Nuxt configuration

Backend (Rust)

Tauri Commands

Commands are the IPC bridge between frontend and backend. They use rename_all = "snake_case", so the JavaScript side must use snake_case parameter names.

Command Returns Description
launch Result<String, String> Checks Java, verifies the server cert against the connection's pin, downloads JARs, launches the administrator. Ok value is a JSON object with a code (see below)
set_pin Result<(), String> Stores the operator-approved leaf-cert fingerprint as the connection's pin
save Result<String, String> Saves a connection entry
delete Result<String, String> Deletes a connection
import Result<String, String> Imports connections from a JSON file (with duplicate detection)
load_connections String Returns all connections as JSON array
load_single_connection Result<Value, String> Returns one connection by ID
get_default_connectionentry Result<Value, String> Returns a default connection template
get_all_groups Result<Value, String> Returns all group names
get_all_engine_types Result<Value, String> Returns all engine-type names
console_subscribe Result<(), String> A console window subscribes to its output: replays the backlog, then streams live lines over a Channel
console_save Result<(), String> Writes the console contents to a chosen file path
get_launcher_info String Returns app version info

The launch Ok string carries a result code so the frontend knows what to do next:

Code Meaning
0 Administrator launched
2 First connect to this server — cert details returned for the trust prompt
3 Server certificate changed — cert details returned for a confirm-or-abort warning
-1 Error — msg describes it (e.g. Java not found, server unreachable)

Launch Flow

  1. Frontend calls launch with a connection ID and a progress Channel
  2. Backend runs a fail-fast java -version check (using the connection's Java Home, else java on PATH). If Java cannot run it returns code -1 with a "Java (with JavaFX) not found" message — before any network work
  3. Captures the server's leaf TLS certificate via a handshake to /webstart.jnlp and computes its SHA-256
  4. Compares that fingerprint to the connection's stored pin (pinnedCertSha256):
    • No pin yet → returns code 2 with cert details so the operator can trust it (TOFU)
    • Fingerprint differs from the pin → returns code 3 with cert details (the cert-changed warning)
    • Fingerprint matches → proceeds. On a trust prompt the frontend calls set_pin and retries launch
  5. Downloads the JNLP and JARs over a pinned rustls client that rejects any leaf whose SHA-256 does not match the pin, sending progress over the Channel. The vendor/version shared cache is reused, skipping downloads whose SHA-256 already matches
  6. Sanitizes JVM arguments (blocks dangerous flags like -javaagent:) and builds the classpath (Mirth/engine JARs first)
  7. Launches the JavaFX administrator, passing the username and password as trailing launch arguments to the main class
  8. If "Show console" is enabled, captures the process's stdout and stderr and opens a native console window — but only after the process spawns successfully, so a failed launch never pops an empty console

Data Storage

  • ConnectionStore manages all connections in a Mutex<HashMap<String, Arc<ConnectionEntry>>>
  • Persisted to ~/.launcher/launcher-data.json (written to a temp file, fsynced, then atomically renamed so a crash can't truncate it). Each entry carries its own pinnedCertSha256
  • JAR cache stored in ~/.launcher/cache/ (vendor/version shared cache)
  • Diagnostic logs in ~/.launcher/logs/
  • There is no separate trust-store file — the pin lives inline on the connection

Security Model

  • The trust boundary is per-connection TLS certificate pinning (trust-on-first-use), implemented in tls.rs. Integration engine servers commonly present self-signed certs, so chain validation is the wrong model
  • On first connect, the launcher captures the server's leaf-cert SHA-256 and prompts the operator to trust that fingerprint. The approved value is pinned to the connection (pinnedCertSha256)
  • On every later connect, the pinned rustls client accepts only a leaf whose SHA-256 equals the pin; a changed cert is rejected and surfaced as the cert-changed warning (code 3)
  • The verifier still validates the TLS handshake signature (proof the server holds the private key) via the ring crypto provider — pinning the public cert bytes alone would let anyone replay them
  • Hostname/SAN is intentionally not checked; the pin replaces it, which is what self-signed engine certs (which rarely carry a matching SAN) require
  • native-tls (vendored) is retained only for the connectivity status probe, which needs reqwest's acceptInvalidHostnames; the launch download path uses the pinned rustls client, not danger_accept_invalid_certs
  • JNLP version attributes are sanitized to prevent path traversal
  • JVM arguments from JNLP are filtered to block code execution flags (-javaagent:, -agentpath:, etc.)

Native Console

When "Show console" is enabled, output is streamed to a dedicated Tauri webview window (label console-<id>) instead of a bundled second Java process. console.rs reads the administrator's stdout and stderr on background threads and pushes each line into a per-window buffer. A window that attaches late replays a capped backlog (MAX_BACKLOG_LINES) and then receives live lines over a Channel, with the buffer lock held across the handover so no line is lost or duplicated. The window closes automatically on a clean exit (status 0) and stays open on a non-zero exit so the error stays readable. Relaunching into an open console bumps a generation counter and shows a "relaunched" separator, so a superseded process's later exit can't flip a live console to "exited".

Tauri Permissions

The main window's capabilities are declared in src-tauri/capabilities/default.json:

Permission Purpose
core:default Core IPC and event system
dialog:default Native file picker and confirm dialogs
core:webview:allow-set-webview-zoom Keyboard zoom support
http:default HTTP requests (scoped to http/https URLs, for server connectivity checks)
shell:allow-open Open URLs in default browser (restricted to the wiki URL only)

Console windows have their own capability in src-tauri/capabilities/console.json (matched by the console-* window glob): core:default, dialog:default, clipboard-manager:allow-write-text, and core:webview:allow-set-webview-zoom.

Frontend (Nuxt 4 / Vue 3)

Design System

Tailwind CSS v4 with @theme design tokens in app/assets/css/main.css:

Token Value Usage
surface-0 #24272e Page background
surface-1 #2c2f37 Cards, inputs
surface-2 #353840 Hover, selected states
surface-3 #3e414a Highest elevation
accent #4f7df7 Primary actions
text-primary #e2e5eb Main text
text-secondary #9ca3b0 Supporting text

Icons use Phosphor Icons via the ph: prefix with the @nuxt/icon module.

Key Patterns

  • Channel API: launch uses tauri::ipc::Channel to stream progress from Rust to the frontend, and console_subscribe uses one to stream console output to a console window
  • Result-code loop: index.vue invokes launch in a bounded loop so a first-use trust (code 2) or cert-change (code 3) can show a modal, call set_pin, and retry, while a cert that keeps changing can't re-prompt forever
  • Imperative modals: useConfirmRejectModal mounts TrustCertModal programmatically for both the first-use trust prompt and the cert-change confirmation, returning a Promise
  • Console windows: each console is a separate Tauri webview window; app.vue detects a console-* window label and renders ConsoleLog.vue, which owns its own light/dark theme and auto-scroll independently of the main window
  • Module-level state: useZoom uses a module-scoped ref for app-wide singleton zoom state
  • CSS tooltips: Custom [data-tooltip] attribute with ::after pseudo-element (native title tooltips are unreliable in Tauri's WKWebView). Use data-tooltip-below for elements near the top of the window
  • localStorage persistence: Sort mode, collapsed groups, zoom level, and console theme all persist to localStorage

Error Handling Conventions

  • Rust: Prefer ? operator and .map_err() over .unwrap(). Return errors to frontend via Result<T, String>. Use .expect("descriptive message") only for mutex locks (poisoning is unrecoverable).
  • Frontend: Wrap invoke calls in try/catch and display errors inline via the error bar at the bottom of the main screen.

Clone this wiki locally