Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego) 183 tests, 76 source files, 9,910 lines of code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
983 lines
51 KiB
Markdown
983 lines
51 KiB
Markdown
# Wraith Desktop — Design Spec
|
||
|
||
> **Date:** 2026-03-17
|
||
> **Purpose:** Native Windows desktop replacement for MobaXTerm — SSH + SFTP + RDP in a single binary
|
||
> **Stack:** Go + Wails v3 (Vue 3 frontend, WebView2) + SQLite + FreeRDP3 (purego)
|
||
> **Target:** Personal tool for daily MSP/sysadmin work — Windows only
|
||
> **Name:** Wraith — exists everywhere, all at once.
|
||
|
||
---
|
||
|
||
## 1. What This Is
|
||
|
||
A Windows desktop application that replaces MobaXTerm. Multi-tabbed SSH terminal with SFTP sidebar (MobaXTerm's killer feature), RDP via FreeRDP3 dynamic linking, connection manager with hierarchical groups, and an encrypted vault for SSH keys and passwords. Ships as `wraith.exe` + `freerdp3.dll`. No Docker, no database server, no sidecar processes.
|
||
|
||
**What this is NOT:** A web app, a SaaS platform, a team tool. It's a personal remote access workstation built as a native desktop binary.
|
||
|
||
**Prior art:** This is a ground-up rebuild of Wraith, which was previously a self-hosted web application (Nuxt 3 + NestJS + guacd + PostgreSQL). The web version proved the feature set; this version delivers it as a proper desktop tool.
|
||
|
||
---
|
||
|
||
## 2. Technology Stack
|
||
|
||
### Backend (Go)
|
||
|
||
| Component | Technology | Purpose |
|
||
|---|---|---|
|
||
| Framework | Wails v3 (alpha) | Desktop app shell, multi-window, Go↔JS bindings |
|
||
| SSH | `golang.org/x/crypto/ssh` | SSH client connections, PTY, auth |
|
||
| SFTP | `github.com/pkg/sftp` | Remote filesystem operations over SSH |
|
||
| RDP | FreeRDP3 via `purego` / `syscall.NewLazyDLL` | RDP protocol, bitmap rendering |
|
||
| Database | SQLite via `modernc.org/sqlite` (pure Go) | Connections, credentials, settings |
|
||
| Encryption | `crypto/aes` + `crypto/cipher` (GCM) | Vault encryption at rest |
|
||
| Key derivation | `golang.org/x/crypto/argon2` | Master password → encryption key |
|
||
|
||
### Frontend (Vue 3 in WebView2)
|
||
|
||
| Component | Technology | Purpose |
|
||
|---|---|---|
|
||
| Framework | Vue 3 (Composition API) | UI framework |
|
||
| Terminal | xterm.js 5.x + WebGL addon | SSH terminal emulator |
|
||
| File editor | CodeMirror 6 | Remote file editing (separate window) |
|
||
| CSS | Tailwind CSS | Utility-first styling |
|
||
| Components | Naive UI | Tree, tabs, modals, dialogs, inputs |
|
||
| State | Pinia | Reactive stores for sessions, connections, app state |
|
||
| Build | Vite | Frontend build tooling |
|
||
|
||
### Distribution
|
||
|
||
| Artifact | Notes |
|
||
|---|---|
|
||
| `wraith.exe` | Single Go binary, ~8-10MB |
|
||
| `freerdp3.dll` | FreeRDP3 dynamic library, shipped alongside |
|
||
| Data | `%APPDATA%\Wraith\wraith.db` (SQLite) |
|
||
| Installer | NSIS via Wails build |
|
||
|
||
---
|
||
|
||
## 3. Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Wails v3 Application (wraith.exe) │
|
||
│ │
|
||
│ ┌─ Go Backend ──────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │ │
|
||
│ │ │ SSH Service │ │ SFTP Service │ │ RDP Service │ │ │
|
||
│ │ │ x/crypto/ssh │ │ pkg/sftp │ │ purego→freerdp3 │ │ │
|
||
│ │ └──────┬───────┘ └──────┬───────┘ └────────┬──────────┘ │ │
|
||
│ │ │ │ │ │ │
|
||
│ │ ┌──────▼─────────────────▼────────────────────▼──────────┐ │ │
|
||
│ │ │ Session Manager │ │ │
|
||
│ │ │ • Tracks all active SSH/RDP sessions │ │ │
|
||
│ │ │ • Routes I/O between frontend and protocol backends │ │ │
|
||
│ │ │ • Supports tab detach/reattach (session ≠ window) │ │ │
|
||
│ │ └────────────────────────┬───────────────────────────────┘ │ │
|
||
│ │ │ │ │
|
||
│ │ ┌────────────────────────▼───────────────────────────────┐ │ │
|
||
│ │ │ Vault Service │ │ │
|
||
│ │ │ • Master password → Argon2id → AES-256-GCM key │ │ │
|
||
│ │ │ • SQLite storage (%APPDATA%\Wraith\wraith.db) │ │ │
|
||
│ │ │ • Encrypts: SSH keys, passwords, RDP credentials │ │ │
|
||
│ │ └────────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
|
||
│ │ │ Connection │ │ Import │ │ Host Key │ │ │
|
||
│ │ │ Manager │ │ .mobaconf │ │ Store │ │ │
|
||
│ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │
|
||
│ └───────────────────────────────────────────────────────────────┘ │
|
||
│ ▲ │
|
||
│ Wails v3 Bindings (type-safe Go↔JS) │
|
||
│ ▼ │
|
||
│ ┌─ Vue 3 Frontend (WebView2) ───────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────────┐ │ │
|
||
│ │ │ xterm.js │ │ SFTP Tree │ │ RDP │ │ CodeMirror 6 │ │ │
|
||
│ │ │ +WebGL │ │ Sidebar │ │ Canvas │ │ (sep window) │ │ │
|
||
│ │ └──────────┘ └───────────┘ └──────────┘ └───────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ ┌───────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ Tab Bar (detachable) + Connection Sidebar │ │ │
|
||
│ │ │ Command Palette (Ctrl+K) | Dark theme │ │ │
|
||
│ │ │ Tailwind CSS + Naive UI │ │ │
|
||
│ │ └───────────────────────────────────────────────────────┘ │ │
|
||
│ └───────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
│ │ │
|
||
SSH (port 22) SFTP (over SSH) RDP (port 3389)
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
Linux/macOS hosts Remote filesystems Windows hosts
|
||
```
|
||
|
||
### Key Architectural Decisions
|
||
|
||
**Sessions ≠ Windows.** SSH and RDP sessions live as objects in the Go Session Manager. The frontend is a view. Detaching a tab spawns a new Wails window pointing at the same backend session. Re-attaching destroys the window and re-renders the session in the original tab. The session itself never drops.
|
||
|
||
**Wails v3 multi-window risk mitigation:** This is the project's biggest technical risk. The detach/reattach model depends on Wails v3's alpha `application.NewWebviewWindow()` API. Three fallback plans, validated in priority order during Phase 1:
|
||
|
||
- **Plan A (target):** Wails v3 `NewWebviewWindow()` — true native multi-window. Spike this in Phase 1 with a minimal two-window prototype before committing.
|
||
- **Plan B:** Single Wails window with internal "floating panel" detach — session renders in a draggable, resizable overlay within the main window. Not true OS windows, but close enough. No external dependency.
|
||
- **Plan C:** Wails v3 server mode — detached sessions open in the default browser at `localhost:{port}/session/{id}`. Functional but breaks the native feel.
|
||
|
||
If Plan A fails, we fall to Plan B (which is entirely within our control). Plan C is the emergency fallback. **This must be validated in Phase 1, not discovered in Phase 4.**
|
||
|
||
**Single binary + DLL.** No Docker, no sidecar processes. SQLite is embedded (pure Go driver). FreeRDP3 is the only external dependency, loaded dynamically via `purego`.
|
||
|
||
**SFTP rides SSH.** SFTP opens a separate SSH channel on the same `x/crypto/ssh` connection as the terminal. No separate TCP connection is needed. `pkg/sftp.NewClient()` takes an `*ssh.Client` (not the shell `*ssh.Session`) and opens its own subsystem channel internally. The terminal shell session and SFTP operate as independent channels multiplexed over the same connection.
|
||
|
||
**RDP via pixel buffer.** FreeRDP3 is loaded via `purego` (dynamic linking, no CGO). FreeRDP writes decoded bitmap frames into a shared Go pixel buffer. The Go backend serves frame data to the frontend via a local HTTP endpoint (`localhost:{random_port}/frame`) that returns raw RGBA data. The frontend renders frames on a `<canvas>` element using `requestAnimationFrame`. Performance target: 1080p @ 30fps using Bitmap Update callbacks. The local HTTP approach is the default; if benchmarking reveals issues, Wails binding with base64-encoded frames is the fallback.
|
||
|
||
---
|
||
|
||
## 4. Data Model (SQLite)
|
||
|
||
```sql
|
||
-- Connection groups (hierarchical folders)
|
||
CREATE TABLE groups (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
parent_id INTEGER REFERENCES groups(id) ON DELETE SET NULL,
|
||
sort_order INTEGER DEFAULT 0,
|
||
icon TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- Saved connections
|
||
CREATE TABLE connections (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
hostname TEXT NOT NULL,
|
||
port INTEGER NOT NULL DEFAULT 22,
|
||
protocol TEXT NOT NULL CHECK(protocol IN ('ssh','rdp')),
|
||
group_id INTEGER REFERENCES groups(id) ON DELETE SET NULL,
|
||
credential_id INTEGER REFERENCES credentials(id) ON DELETE SET NULL,
|
||
color TEXT,
|
||
tags TEXT DEFAULT '[]', -- JSON array: ["Prod","Linux","Client-RSM"]
|
||
notes TEXT,
|
||
options TEXT DEFAULT '{}', -- JSON: protocol-specific settings
|
||
sort_order INTEGER DEFAULT 0,
|
||
last_connected DATETIME,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- Credentials (password or SSH key reference)
|
||
CREATE TABLE credentials (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
username TEXT,
|
||
domain TEXT,
|
||
type TEXT NOT NULL CHECK(type IN ('password','ssh_key')),
|
||
encrypted_value TEXT,
|
||
ssh_key_id INTEGER REFERENCES ssh_keys(id) ON DELETE SET NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- SSH private keys (encrypted at rest)
|
||
CREATE TABLE ssh_keys (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
key_type TEXT,
|
||
fingerprint TEXT,
|
||
public_key TEXT,
|
||
encrypted_private_key TEXT NOT NULL,
|
||
passphrase_encrypted TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- Terminal themes (16-color ANSI + fg/bg/cursor)
|
||
CREATE TABLE themes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL UNIQUE,
|
||
foreground TEXT NOT NULL,
|
||
background TEXT NOT NULL,
|
||
cursor TEXT NOT NULL,
|
||
black TEXT NOT NULL,
|
||
red TEXT NOT NULL,
|
||
green TEXT NOT NULL,
|
||
yellow TEXT NOT NULL,
|
||
blue TEXT NOT NULL,
|
||
magenta TEXT NOT NULL,
|
||
cyan TEXT NOT NULL,
|
||
white TEXT NOT NULL,
|
||
bright_black TEXT NOT NULL,
|
||
bright_red TEXT NOT NULL,
|
||
bright_green TEXT NOT NULL,
|
||
bright_yellow TEXT NOT NULL,
|
||
bright_blue TEXT NOT NULL,
|
||
bright_magenta TEXT NOT NULL,
|
||
bright_cyan TEXT NOT NULL,
|
||
bright_white TEXT NOT NULL,
|
||
selection_bg TEXT,
|
||
selection_fg TEXT,
|
||
is_builtin BOOLEAN DEFAULT 0
|
||
);
|
||
|
||
-- Connection history (for recent connections + frequency sorting)
|
||
CREATE TABLE connection_history (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
connection_id INTEGER NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
|
||
protocol TEXT NOT NULL,
|
||
connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
disconnected_at DATETIME,
|
||
duration_secs INTEGER
|
||
);
|
||
|
||
-- Known SSH host keys
|
||
CREATE TABLE host_keys (
|
||
hostname TEXT NOT NULL,
|
||
port INTEGER NOT NULL,
|
||
key_type TEXT NOT NULL,
|
||
fingerprint TEXT NOT NULL,
|
||
raw_key TEXT,
|
||
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (hostname, port, key_type)
|
||
);
|
||
|
||
-- App settings (key-value)
|
||
CREATE TABLE settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
**`connections.options`** — JSON blob for protocol-specific settings. SSH: keepalive interval, preferred auth method, shell integration toggle. RDP: color depth, security mode (NLA/TLS/RDP), console session, audio redirection, display resolution. Keeps the schema clean and extensible as we discover edge cases without adding nullable columns.
|
||
|
||
**`connections.tags`** — JSON array searchable via SQLite's `json_each()`. Enables filtering across groups (type "Prod" in search, see only production hosts regardless of which group they're in).
|
||
|
||
**Connections → Credentials** is many-to-one. Multiple hosts can share the same credential.
|
||
|
||
**SQLite WAL mode:** Enable Write-Ahead Logging (`PRAGMA journal_mode=WAL`) on database open in `db/sqlite.go`. WAL mode allows concurrent reads during writes, preventing "database is locked" errors when the frontend queries connections while the backend is writing session history or updating `last_connected` timestamps. Also set `PRAGMA busy_timeout=5000` as a safety net.
|
||
|
||
**Host keys** are keyed by `(hostname, port, key_type)`. Supports multiple key types per host. Separated from connections so host key verification works independently of saved connections (e.g., quick connect).
|
||
|
||
---
|
||
|
||
## 5. UI Layout
|
||
|
||
### Visual Identity
|
||
|
||
Dark theme inspired by the Wraith brand: deep dark backgrounds (#0d1117), blue accent (#58a6ff), green for SSH indicators (#3fb950), blue for RDP indicators (#1f6feb). The aesthetic is "operator command center" — atmospheric, moody, professional. Reference: `docs/karens-wraith-layout.png` for the target mood.
|
||
|
||
Logo: `images/wraith-logo.png` — ghost with "$" symbol, used in the title bar and app icon.
|
||
|
||
**The "alive" feel:** Tabs use a 0.5s CSS `transition` on `background-color` and `border-color` when switching between active and backgrounded states. The active tab's background subtly brightens; backgrounded tabs dim. This creates a fluid, "breathing" quality as you switch between sessions — the Wraith is present without being loud. Same 0.5s transition applies to sidebar item hover states and toolbar button interactions. No animations on the terminal itself — that would be distracting.
|
||
|
||
### Main Window Layout
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ [👻 WRAITH v1.0] File View Tools Settings Help │ ← Title/Menu bar
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ [⚡ Quick connect...] [+SSH] [+RDP] 4 sessions 🔒 ⚙ │ ← Toolbar
|
||
├────────────┬────────────────────────────────────────────────┤
|
||
│ │ [Asgard ●] [Docker ●] [Predator ●] [VM01 ●]+ │ ← Tab bar
|
||
│ SIDEBAR │────────────────────────────────────────────────│
|
||
│ │ │
|
||
│ Toggles: │ Terminal / RDP Canvas │
|
||
│ 📂 Conn │ │
|
||
│ 📁 SFTP │ (xterm.js or <canvas>) │
|
||
│ │ │
|
||
│ Search │ Primary workspace area │
|
||
│ Tags │ Takes dominant space │
|
||
│ Groups │ │
|
||
│ Tree │ │
|
||
│ │ │
|
||
├────────────┴────────────────────────────────────────────────┤
|
||
│ SSH · root@asgard:22 ⚠️ ↑1.2K ↓3.4K Dark+ UTF-8 120×40 │ ← Status bar
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Sidebar Behavior
|
||
|
||
The left sidebar is a **single panel that toggles context** between Connections and SFTP (same as MobaXTerm, not two panels):
|
||
|
||
- **Connections view:** Search bar, tag filter pills, recent connections, hierarchical group tree with connection entries. Green dots for SSH, blue dots for RDP. "Connected" indicator on active sessions. Right-click context menu for edit, delete, duplicate, move to group.
|
||
- **SFTP view:** Activates when an SSH session connects. Path bar showing current remote directory. Toolbar with upload, download, new file, new folder, refresh, delete. File tree with name, size, modified date. "Follow terminal folder" toggle at bottom.
|
||
|
||
The sidebar is resizable. Minimum width ~200px, collapsible to icon-only rail.
|
||
|
||
### Tab Bar
|
||
|
||
- Color-coded dots: green = SSH, blue = RDP
|
||
- Protocol icon on each tab
|
||
- Environment badges: optional colored pills (PROD, ROOT, DEV) derived from connection tags
|
||
- Root session warning: tabs connected as root get a subtle warm accent
|
||
- Close button (×) on each tab
|
||
- Pop-out icon (↗) on hover for tab detach
|
||
- Overflow: chevron dropdown for hidden tabs when 10+ are open (not multi-line rows)
|
||
- Drag to reorder tabs
|
||
- Drag out of tab bar to detach into new window
|
||
- "+" button to open new session
|
||
|
||
### Tab Detach/Reattach
|
||
|
||
- **Detach:** Drag tab out of bar OR click ↗ icon → spawns new Wails window with that session still alive. Original tab shows "Session detached — [Reattach]" placeholder.
|
||
- **Reattach:** Click "Reattach" button in placeholder OR close the detached window → session snaps back into the tab bar.
|
||
- Works for both SSH and RDP sessions.
|
||
- The session lives in the Go backend, not the window. Detaching is just moving the view.
|
||
|
||
### Command Palette (Ctrl+K)
|
||
|
||
Modal overlay with fuzzy search across:
|
||
- Connection names, hostnames, group names, tags
|
||
- Actions: "New SSH", "New RDP", "Open Vault", "Settings", "Import MobaXTerm"
|
||
- Active sessions: "Switch to Asgard", "Disconnect Docker"
|
||
- Keyboard-first — arrow keys to navigate, Enter to select, Esc to close
|
||
|
||
### Status Bar
|
||
|
||
- Left: Protocol, user@host:port, privilege warning (⚠️ when root), transfer speed
|
||
- Right: Active theme name, encoding (UTF-8), terminal dimensions (cols×rows)
|
||
- Active session count in toolbar area
|
||
|
||
### Terminal Theming
|
||
|
||
Built-in themes: Dracula, Nord, Monokai, Solarized Dark, One Dark, Gruvbox, plus a "MobaXTerm Classic" theme matching the colors from the user's `.mobaconf` export.
|
||
|
||
Custom theme creation via settings. Full 16-color ANSI palette + foreground/background/cursor maps directly to xterm.js `ITheme` objects.
|
||
|
||
Per-connection theme override via `connections.options` JSON field.
|
||
|
||
### Keyboard Shortcuts
|
||
|
||
| Shortcut | Action |
|
||
|---|---|
|
||
| Ctrl+K | Command palette |
|
||
| Ctrl+T | New SSH session (from quick connect) |
|
||
| Ctrl+W | Close current tab |
|
||
| Ctrl+Tab | Next tab |
|
||
| Ctrl+Shift+Tab | Previous tab |
|
||
| Ctrl+1-9 | Switch to tab N |
|
||
| Ctrl+B | Toggle sidebar |
|
||
| Ctrl+Shift+D | Detach current tab |
|
||
| F11 | Fullscreen |
|
||
| Ctrl+Shift+C | Copy (terminal) |
|
||
| Ctrl+Shift+V | Paste (terminal) |
|
||
| Ctrl+F | Search in terminal scrollback |
|
||
|
||
### CodeMirror 6 Editor (Separate Window)
|
||
|
||
- Opens as a new Wails window when clicking a text file in the SFTP sidebar
|
||
- File size guard: files over 5MB refused for inline editing (offered download instead)
|
||
- Syntax highlighting based on file extension
|
||
- Save button writes content back to remote via SFTP
|
||
- Unsaved changes warning on close
|
||
- Window title: `filename — host — Wraith Editor`
|
||
|
||
---
|
||
|
||
## 6. SSH + SFTP Flow
|
||
|
||
### SSH Connection
|
||
|
||
```
|
||
User double-clicks "Asgard" in connection sidebar
|
||
→ Go: ConnectionManager.Connect(connectionId)
|
||
→ Go: VaultService.DecryptCredential(credentialId) → auth method
|
||
→ Go: SSHService.Dial(hostname, port, authConfig)
|
||
→ x/crypto/ssh.Dial() with host key callback
|
||
→ If new host key: emit event to frontend, user accepts/rejects
|
||
→ If changed host key: BLOCK connection, warn user (no silent accept)
|
||
→ If accepted: store in host_keys table
|
||
→ Go: SessionManager.Create(sshClient, connectionId) → sessionId
|
||
→ Go: SSHService.RequestPTY(session, "xterm-256color", cols, rows)
|
||
→ Go: SSHService.Shell(session) → stdin/stdout pipes
|
||
→ Frontend: xterm.js instance created, bound to sessionId
|
||
→ Wails bindings: bidirectional data flow
|
||
→ xterm.js onData → Go SSHService.Write(sessionId, bytes)
|
||
→ Go SSHService.Read(sessionId) → Wails event → xterm.js write
|
||
```
|
||
|
||
### SSH Authentication
|
||
|
||
Supports three auth methods:
|
||
|
||
1. **SSH Key:** Decrypt private key from vault. If key has passphrase, decrypt that too. Pass to `ssh.PublicKeys()` signer.
|
||
2. **Password:** Decrypt password from vault. Pass to `ssh.Password()`.
|
||
3. **Keyboard-Interactive:** For servers with 2FA/MFA prompts. `ssh.KeyboardInteractive()` callback relays challenge prompts to the frontend, user responds in a dialog. Common in MSP environments with PAM-based MFA.
|
||
|
||
Auth methods are tried in order: key → password → keyboard-interactive. The credential type determines which are attempted first, but keyboard-interactive is always available as a fallback for servers that require it.
|
||
|
||
### Terminal Resize
|
||
|
||
```
|
||
Frontend: xterm.js fit addon detects container resize
|
||
→ Wails binding: SSHService.Resize(sessionId, cols, rows)
|
||
→ Go: session.WindowChange(rows, cols)
|
||
```
|
||
|
||
### SFTP Sidebar
|
||
|
||
```
|
||
SSH connection established:
|
||
→ Go: SFTPService.Open(sshClient) → pkg/sftp.NewClient(sshClient)
|
||
→ Go: SFTPService.List(sessionId, homeDir) → directory listing
|
||
→ Frontend: sidebar switches to SFTP view, renders file tree
|
||
```
|
||
|
||
SFTP uses the **same SSH connection** as the terminal (SFTP subsystem). No separate connection needed.
|
||
|
||
**File operations:** All SFTP commands route through Go via Wails bindings, targeting the correct `pkg/sftp` client by sessionId.
|
||
|
||
| Operation | Go function | Notes |
|
||
|---|---|---|
|
||
| List directory | `sftp.ReadDir(path)` | Lazy-loaded on tree expand |
|
||
| Upload | `sftp.Create(path)` + chunked write | Drag-and-drop from Windows Explorer |
|
||
| Download | `sftp.Open(path)` + read | Browser-style save dialog |
|
||
| Delete | `sftp.Remove(path)` / `sftp.RemoveAll(path)` | Confirmation prompt |
|
||
| Rename/Move | `sftp.Rename(old, new)` | |
|
||
| Mkdir | `sftp.Mkdir(path)` | |
|
||
| Chmod | `sftp.Chmod(path, mode)` | |
|
||
| Read file | `sftp.Open(path)` → content | Opens in CodeMirror window |
|
||
| Write file | `sftp.Create(path)` ← content | Save from CodeMirror |
|
||
|
||
### CWD Following
|
||
|
||
```
|
||
SSH session starts:
|
||
→ Go: injects shell hook after PTY is established:
|
||
PROMPT_COMMAND='printf "\033]7;file://%s%s\033\\" "$(hostname)" "$PWD"'
|
||
(or precmd for zsh)
|
||
→ Go: SSHService reads stdout, scans for OSC 7 escape sequences
|
||
→ Go: strips OSC 7 before forwarding to xterm.js (user never sees it)
|
||
→ Go: emits CWD change event with new path
|
||
→ Frontend: if "Follow terminal folder" is enabled, calls SFTPService.List(newPath)
|
||
→ Frontend: SFTP tree navigates to new directory
|
||
```
|
||
|
||
"Follow terminal folder" is a per-session toggle (checkbox at bottom of SFTP sidebar), enabled by default.
|
||
|
||
**Shell detection:** The OSC 7 injection assumes a bash-like shell (`PROMPT_COMMAND`) or zsh (`precmd`). For fish, the equivalent is `function fish_prompt; printf "\033]7;file://%s%s\033\\" (hostname) "$PWD"; end`. If shell detection fails (unknown shell, restricted shell, non-interactive session), CWD following is silently disabled — the SFTP sidebar stays at the initial home directory and requires manual navigation.
|
||
|
||
### Upload Flow
|
||
|
||
```
|
||
User drags file from Windows Explorer onto SFTP sidebar:
|
||
→ Frontend: reads file via File API, sends chunks to Go
|
||
→ Go: SFTPService.Upload(sessionId, remotePath, fileData)
|
||
→ Go: sftp.Create(remotePath) → write chunks → close
|
||
→ Progress events emitted back to frontend
|
||
→ SFTP tree refreshes on completion
|
||
```
|
||
|
||
---
|
||
|
||
## 7. RDP Flow
|
||
|
||
### Architecture
|
||
|
||
FreeRDP3 is loaded via `purego` (or `syscall.NewLazyDLL`) at runtime. No CGO, no C compiler needed. The Go binary loads `freerdp3.dll` from the application directory.
|
||
|
||
### Connection
|
||
|
||
```
|
||
User double-clicks "CLT-VMHOST01" in connection sidebar:
|
||
→ Go: ConnectionManager.Connect(connectionId)
|
||
→ Go: VaultService.DecryptCredential(credentialId) → username, password, domain
|
||
→ Go: RDPService.Connect(host, port, username, password, domain, options)
|
||
→ purego: freerdp_new() → configure settings → freerdp_connect()
|
||
→ Register BitmapUpdate callback
|
||
→ Go: allocate pixel buffer (width × height × 4 bytes RGBA)
|
||
→ FreeRDP: decoded bitmap frames written into pixel buffer
|
||
→ Go: frame data served to frontend
|
||
→ Frontend: <canvas> renders frames via requestAnimationFrame (30fps target)
|
||
```
|
||
|
||
### Frame Delivery
|
||
|
||
FreeRDP writes decoded frame data into a shared Go pixel buffer. The frontend retrieves frame data via one of:
|
||
|
||
- **Local HTTP endpoint:** `localhost:{random_port}/frame` returns raw RGBA or PNG
|
||
- **Blob URL:** Go encodes frame, passes via Wails binding as base64
|
||
- **Optimal approach TBD during implementation** — benchmark both
|
||
|
||
Performance target: **1080p @ 30fps**. Focus on Bitmap Update callbacks. No H.264 pipeline needed — raw bitmap updates with basic RLE compression is sufficient for remote management work.
|
||
|
||
### Input Handling
|
||
|
||
```
|
||
Frontend: mouse/keyboard events captured on <canvas> element
|
||
→ Wails binding → Go: RDPService.SendMouseEvent(sessionId, x, y, flags)
|
||
→ Wails binding → Go: RDPService.SendKeyEvent(sessionId, keycode, pressed)
|
||
→ Go: translate JS virtual keycodes to RDP scancodes via lookup table
|
||
→ Go: purego calls freerdp_input_send_mouse_event / freerdp_input_send_keyboard_event
|
||
```
|
||
|
||
**Scancode mapping:** JavaScript `KeyboardEvent.code` values (e.g., "KeyA", "ShiftLeft") must be translated to RDP hardware scancodes that FreeRDP expects. A static lookup table in `internal/rdp/input.go` maps JS key codes → RDP scancodes. This is a known complexity in web-based RDP — the table must handle extended keys (e.g., right Alt, numpad) and platform-specific quirks. Reference: FreeRDP's `scancode.h` for the canonical scancode list.
|
||
|
||
**System key pass-through:** The Windows key and Alt+Tab require special handling. By default, these keys are captured by the local OS. A per-connection toggle in `connections.options` (`"grabKeyboard": true`) controls whether system keys are forwarded to the remote host or stay local. When enabled, the RDP canvas captures all keyboard input including Win key, Alt+Tab, Ctrl+Alt+Del (via a toolbar button). Power users toggling between remote and local need this to be fast and obvious — surface it as an icon in the RDP toolbar.
|
||
|
||
### Clipboard Sync
|
||
|
||
```
|
||
Remote → Local:
|
||
→ Go: FreeRDP clipboard channel callback fires
|
||
→ Go: emits clipboard event to frontend
|
||
→ Frontend: writes to system clipboard via Wails API
|
||
|
||
Local → Remote:
|
||
→ Frontend: detects clipboard change (or user pastes)
|
||
→ Wails binding → Go: RDPService.SendClipboard(sessionId, data)
|
||
→ Go: writes to FreeRDP clipboard channel
|
||
```
|
||
|
||
### RDP Connection Options
|
||
|
||
Stored in `connections.options` JSON field:
|
||
|
||
```json
|
||
{
|
||
"colorDepth": 32,
|
||
"security": "nla",
|
||
"consoleSession": false,
|
||
"audioRedirect": false,
|
||
"width": 1920,
|
||
"height": 1080,
|
||
"scaleFactor": 100
|
||
}
|
||
```
|
||
|
||
**HiDPI / display scaling:** On Windows with display scaling (e.g., 150% on a 4K monitor), the RDP session resolution must account for the scale factor. `scaleFactor` in connection options controls whether to send the physical pixel resolution or the scaled logical resolution to FreeRDP. Default behavior: detect the current Windows DPI setting and scale the RDP resolution accordingly. Override via the `scaleFactor` option (100 = no scaling, 150 = 150%).
|
||
|
||
---
|
||
|
||
## 8. Vault + Encryption
|
||
|
||
### Master Password Flow
|
||
|
||
```
|
||
App launch:
|
||
→ Master password prompt (modal, cannot be bypassed)
|
||
→ If first launch:
|
||
→ Generate random 32-byte salt
|
||
→ Store salt in settings table (key: "vault_salt")
|
||
→ Derive key: Argon2id(password, salt, t=3, m=65536, p=4, keyLen=32)
|
||
→ Encrypt a known test value ("wraith-vault-check") with derived key
|
||
→ Store encrypted test value in settings (key: "vault_check")
|
||
→ If returning:
|
||
→ Read salt and encrypted test value from settings
|
||
→ Derive key with same parameters
|
||
→ Attempt to decrypt test value
|
||
→ If decryption succeeds → vault unlocked
|
||
→ If fails → wrong password, prompt again
|
||
→ Derived key held in memory only, never written to disk
|
||
→ Key zeroed from memory on app close
|
||
```
|
||
|
||
### Encryption Functions
|
||
|
||
```
|
||
Encrypt(plaintext string) → string:
|
||
→ Generate random 12-byte IV (crypto/rand)
|
||
→ AES-256-GCM Seal(): returns ciphertext with authTag appended (Go's native format)
|
||
→ Return "v1:{iv_hex}:{sealed_hex}"
|
||
→ (sealed = ciphertext || authTag, as produced by cipher.AEAD.Seal())
|
||
|
||
Decrypt(blob string) → string:
|
||
→ Parse version prefix, IV (12B), sealed data
|
||
→ AES-256-GCM Open(): decrypts and verifies authTag (Go's native format)
|
||
→ Return plaintext
|
||
```
|
||
|
||
The `v1:` version prefix enables future key rotation without re-encrypting all stored values.
|
||
|
||
### What Gets Encrypted
|
||
|
||
| Data | Encrypted | Reason |
|
||
|---|---|---|
|
||
| SSH private keys | Yes | Sensitive key material |
|
||
| SSH key passphrases | Yes | Passphrase is a secret |
|
||
| Password credentials | Yes | Passwords are secrets |
|
||
| RDP passwords | Yes | Via credential reference |
|
||
| Hostnames, ports, usernames | No | Not secrets, needed for display |
|
||
| Public keys, fingerprints | No | Public by definition |
|
||
| Group names, tags, notes | No | Not secrets |
|
||
| Settings, themes | No | User preferences |
|
||
|
||
### Argon2id Parameters
|
||
|
||
| Parameter | Value | Rationale |
|
||
|---|---|---|
|
||
| Time cost (t) | 3 | OWASP recommended minimum |
|
||
| Memory cost (m) | 65536 (64MB) | Resists GPU attacks |
|
||
| Parallelism (p) | 4 | Matches typical core count |
|
||
| Key length | 32 bytes (256-bit) | AES-256 key size |
|
||
| Salt | 32 bytes, random | Unique per installation |
|
||
|
||
### Future: Windows DPAPI Integration (Post-MVP)
|
||
|
||
The current vault is secure and portable (works on any Windows machine, backup the `.db` file and go). Post-MVP, an optional DPAPI layer could wrap the derived AES key with Windows Data Protection API, tying the vault to the current Windows user account. This would enable:
|
||
|
||
- Transparent unlock when logged into Windows (no master password prompt)
|
||
- Hardware-backed key protection on machines with TPM
|
||
- Enterprise trust (DPAPI is a known quantity for IT departments)
|
||
|
||
Implementation: the Argon2id-derived key gets wrapped with `CryptProtectData()` and stored. On unlock, DPAPI unwraps the key. Master password remains the fallback for portability (moving the database to another machine). This is designed-for but not built in MVP — the `v1:` encryption prefix enables adding a `v2:` scheme without re-encrypting existing data.
|
||
|
||
---
|
||
|
||
## 9. MobaXTerm Importer
|
||
|
||
### Config Format
|
||
|
||
MobaXTerm exports configuration as `.mobaconf` files — INI format with `%`-delimited session strings.
|
||
|
||
```ini
|
||
[Bookmarks_1]
|
||
SubRep=AAA Vantz's Stuff # Group name
|
||
ImgNum=41 # Icon index
|
||
*Asgard=#109#0%192.168.1.4%22%vstockwell%... # SSH session
|
||
CLT-VMHOST01=#91#4%100.64.1.204%3389%... # RDP session
|
||
|
||
[SSH_Hostkeys]
|
||
ssh-ed25519@22:192.168.1.4=0x29ac... # Known host keys
|
||
|
||
[Colors]
|
||
ForegroundColour=236,236,236 # Terminal colors
|
||
BackgroundColour=36,36,36
|
||
|
||
[Passwords]
|
||
vstockwell@192.168.1.214=_@9jajOXK... # Encrypted (can't import)
|
||
```
|
||
|
||
### Session String Parsing
|
||
|
||
| Protocol | Type code | Fields (%-delimited) |
|
||
|---|---|---|
|
||
| SSH | `#109#` | host, port, username, ..., SSH key path, ..., colors |
|
||
| RDP | `#91#` | host, port, username, ..., color depth, security |
|
||
|
||
### Import Flow
|
||
|
||
```
|
||
1. User: File → Import → Select .mobaconf file
|
||
2. Go: parse INI sections
|
||
3. Go: extract groups from [Bookmarks_N] SubRep values
|
||
4. Go: parse session strings → connections
|
||
5. Go: parse [SSH_Hostkeys] → host_keys table
|
||
6. Go: parse [Colors] + [Font] → create "MobaXTerm Import" theme
|
||
7. Frontend: show preview dialog:
|
||
"Found: 18 connections, 1 group, 4 host keys, 1 color theme"
|
||
8. User confirms import
|
||
9. Go: create groups, connections, host keys, theme in SQLite
|
||
10. Frontend: report results:
|
||
"Imported! 5 connections reference SSH keys — re-import key files.
|
||
3 connections had stored passwords — re-enter in Wraith vault."
|
||
```
|
||
|
||
### What Gets Imported
|
||
|
||
| Data | Imported | Notes |
|
||
|---|---|---|
|
||
| Connection names | Yes | |
|
||
| Groups (folder hierarchy) | Yes | From SubRep values |
|
||
| Hostnames, ports | Yes | |
|
||
| Usernames | Yes | |
|
||
| Protocol (SSH/RDP) | Yes | From type code #109# / #91# |
|
||
| SSH key file paths | As notes | User must re-import actual key files |
|
||
| Host keys | Yes | To host_keys table |
|
||
| Terminal colors | Yes | As a new theme |
|
||
| Font preferences | Yes | To settings |
|
||
| Encrypted passwords | No | MobaXTerm-encrypted, can't decrypt |
|
||
|
||
---
|
||
|
||
## 10. Frontend Structure
|
||
|
||
```
|
||
frontend/
|
||
src/
|
||
App.vue # Root: master password → main layout
|
||
layouts/
|
||
MainLayout.vue # Sidebar + tab container + status bar
|
||
UnlockLayout.vue # Master password prompt
|
||
components/
|
||
sidebar/
|
||
ConnectionTree.vue # Group tree with connection entries
|
||
SftpBrowser.vue # SFTP file tree + toolbar
|
||
SidebarToggle.vue # Connections ↔ SFTP toggle
|
||
session/
|
||
SessionContainer.vue # Holds all active sessions (v-show, not v-if)
|
||
TabBar.vue # Draggable, detachable tab bar
|
||
TabBadge.vue # PROD/ROOT/DEV environment pills
|
||
terminal/
|
||
TerminalView.vue # xterm.js instance wrapper
|
||
ThemePicker.vue # Terminal color scheme selector
|
||
rdp/
|
||
RdpView.vue # Canvas-based RDP renderer
|
||
RdpToolbar.vue # Clipboard, fullscreen controls
|
||
sftp/
|
||
FileTree.vue # Remote filesystem tree (lazy-loaded)
|
||
TransferProgress.vue # Upload/download progress indicator
|
||
vault/
|
||
VaultManager.vue # SSH keys + credentials management
|
||
KeyImportDialog.vue # SSH key import modal
|
||
CredentialForm.vue # Password/key credential form
|
||
common/
|
||
CommandPalette.vue # Ctrl+K fuzzy search overlay
|
||
QuickConnect.vue # Quick connect input
|
||
StatusBar.vue # Bottom status bar
|
||
composables/
|
||
useSession.ts # Session lifecycle + tab management
|
||
useTerminal.ts # xterm.js + Wails binding bridge
|
||
useSftp.ts # SFTP operations via Wails bindings
|
||
useRdp.ts # RDP canvas rendering + input capture
|
||
useVault.ts # Key/credential CRUD
|
||
useConnections.ts # Connection CRUD + search + tags
|
||
useTheme.ts # Terminal theme management
|
||
useCommandPalette.ts # Command palette search + actions
|
||
stores/
|
||
session.store.ts # Active sessions, tab order, detach state
|
||
connection.store.ts # Connections, groups, search state
|
||
app.store.ts # Global state: unlocked, settings, active theme
|
||
```
|
||
|
||
**Session architecture:** Active sessions render as persistent components inside `SessionContainer.vue`. Switching tabs toggles `v-show` visibility (not `v-if` destruction), so xterm.js and RDP canvas instances stay alive across tab switches. This is critical — destroying and recreating terminal instances would lose scrollback and session state.
|
||
|
||
---
|
||
|
||
## 11. Go Backend Structure
|
||
|
||
```
|
||
internal/
|
||
app/
|
||
app.go # Wails app setup, window management
|
||
menu.go # Application menu definitions
|
||
session/
|
||
manager.go # Session lifecycle, tab detach/reattach
|
||
session.go # Session struct (SSH or RDP, backend state)
|
||
ssh/
|
||
service.go # SSH dial, PTY, shell, I/O pipes
|
||
hostkey.go # Host key verification + storage
|
||
cwd.go # OSC 7 parsing for CWD tracking
|
||
sftp/
|
||
service.go # SFTP operations (list, upload, download, etc.)
|
||
rdp/
|
||
service.go # RDP session management
|
||
freerdp.go # purego bindings to freerdp3.dll
|
||
pixelbuffer.go # Shared frame buffer management
|
||
input.go # Mouse/keyboard event translation
|
||
vault/
|
||
service.go # Encrypt/decrypt, master password, key derivation
|
||
vault_test.go # Encryption round-trip tests
|
||
connections/
|
||
service.go # Connection CRUD, group management
|
||
search.go # Full-text search + tag filtering
|
||
importer/
|
||
mobaconf.go # MobaXTerm .mobaconf parser
|
||
mobaconf_test.go # Parser tests with real config samples
|
||
db/
|
||
sqlite.go # SQLite connection, migrations
|
||
migrations/ # SQL migration files
|
||
settings/
|
||
service.go # Key-value settings CRUD
|
||
theme/
|
||
service.go # Theme CRUD, built-in theme definitions
|
||
builtins.go # Dracula, Nord, Monokai, etc.
|
||
plugin/
|
||
interfaces.go # Plugin interfaces (ProtocolHandler, Importer, etc.)
|
||
registry.go # Plugin registration and lifecycle
|
||
```
|
||
|
||
### Plugin Interface
|
||
|
||
Wraith exposes Go interfaces that community developers can implement to extend functionality:
|
||
|
||
```go
|
||
// ProtocolHandler — add support for new protocols (VNC, Telnet, etc.)
|
||
type ProtocolHandler interface {
|
||
Name() string
|
||
Connect(config ConnectionConfig) (Session, error)
|
||
Disconnect(sessionId string) error
|
||
}
|
||
|
||
// Importer — add support for importing from other tools
|
||
type Importer interface {
|
||
Name() string
|
||
FileExtensions() []string
|
||
Parse(data []byte) (*ImportResult, error)
|
||
}
|
||
```
|
||
|
||
Plugins are compiled into the binary (not runtime-loaded). Community developers fork the repo, implement the interface, register it in `plugin/registry.go`, and build. This keeps distribution simple (single binary) while enabling extensibility.
|
||
|
||
---
|
||
|
||
## 12. MVP Scope
|
||
|
||
### In MVP (launch-blocking)
|
||
|
||
| Feature | Priority | Phase | Notes |
|
||
|---|---|---|---|
|
||
| Wails v3 scaffold + SQLite + vault | P0 | 1 | Foundation — nothing works without this |
|
||
| Connection manager sidebar | P0 | 1 | Groups, tree, search, tags |
|
||
| SSH terminal (xterm.js) | P0 | 2 | Multi-tab, 8+ concurrent sessions |
|
||
| SFTP sidebar | P0 | 2 | Auto-open, CWD following, file ops |
|
||
| Credential vault UI | P0 | 2 | SSH key import, credential management |
|
||
| Host key verification | P0 | 2 | Accept/reject new, block changed |
|
||
| RDP in tabs | P0 | 3 | FreeRDP3/purego, embedded canvas |
|
||
| MobaXTerm importer | P1 | 4 | Parse .mobaconf, first-run detection |
|
||
| Terminal theming | P1 | 4 | 6+ built-in themes, custom themes |
|
||
| Tab detach/reattach | P1 | 4 | Drag out, pop-out icon, reattach button |
|
||
| CodeMirror 6 editor | P1 | 4 | Separate window, syntax highlighting |
|
||
| Command palette (Ctrl+K) | P1 | 4 | Fuzzy search connections + actions |
|
||
| Session context awareness | P1 | 4 | Root warning, user@host in status bar |
|
||
| Tab badges | P1 | 4 | Protocol icon, environment tags |
|
||
| Quick connect | P1 | 4 | user@host:port in toolbar |
|
||
| Plugin interface | P1 | 1 | Define interfaces, implement in later phases |
|
||
| README.md | P1 | 1 | Developer docs, architecture, contribution guide |
|
||
|
||
### Post-MVP
|
||
|
||
| Feature | Notes |
|
||
|---|---|
|
||
| Split panes | Horizontal/vertical splits within a tab |
|
||
| Session recording/playback | asciinema-compatible |
|
||
| Jump host / bastion proxy | ProxyJump chain support |
|
||
| Port forwarding manager | Local, remote, dynamic SSH tunnels |
|
||
| Saved snippets/macros | Quick-execute command library |
|
||
| Tab grouping/stacking | Browser-style tab groups |
|
||
| Live latency monitoring | Ping/packet loss in status bar |
|
||
| Dual-pane SFTP | Server-to-server file operations |
|
||
| Auto-detect environment | Parse hostname for prod/dev/staging classification |
|
||
| Subtle glow effects | "Wraith" personality — energy on active sessions |
|
||
| Dynamic plugin loading | Drop-in plugins without recompilation (longer-term) |
|
||
| Windows DPAPI vault | Optional OS-backed encryption layer for transparent unlock |
|
||
| **Claude Code plugin** | **First official plugin — see below** |
|
||
|
||
### Post-MVP Plugin: Claude Code Integration
|
||
|
||
The first plugin built on the Wraith plugin interface. Embeds Claude Code directly into Wraith as a sidebar panel or tab, with full access to the active session's context.
|
||
|
||
**Authentication:** User authenticates with their Anthropic API key or Claude account (stored encrypted in the vault alongside SSH keys and passwords). Key is decrypted on demand, never persisted in plaintext.
|
||
|
||
**Core capabilities:**
|
||
- **Terminal integration:** Claude Code runs in a dedicated Wraith tab (xterm.js instance). It can see the active SSH session's terminal output and type commands into it — same as a human operator switching tabs.
|
||
- **SFTP-aware file access:** Claude Code can read and write files on the remote host via the active SFTP session. "Read `/etc/nginx/nginx.conf`" pulls the file through SFTP, Claude analyzes/modifies it, and writes it back. No need for Claude to SSH separately — it rides the existing Wraith session.
|
||
- **CodeMirror handoff:** Claude can open files in the CodeMirror editor window, make changes, and save back to the remote host. The user sees the edits happening in real-time.
|
||
- **Context awareness:** Claude sees which host you're connected to, the current working directory (via CWD tracking), and recent terminal output. "Fix the nginx config on this server" just works because Claude already knows where "this" is.
|
||
|
||
**UX flow:**
|
||
1. User opens Claude Code panel (sidebar tab or dedicated session tab)
|
||
2. Types a prompt: "Check why nginx is returning 502 on this server"
|
||
3. Claude reads recent terminal output, pulls nginx config via SFTP, analyzes logs
|
||
4. Claude proposes a fix, user approves, Claude writes the file via SFTP
|
||
5. Claude types `nginx -t && systemctl reload nginx` into the terminal
|
||
|
||
**Plugin interface usage:** This plugin implements `ProtocolHandler` (for the Claude Code tab) and extends the SFTP/terminal services to allow programmatic read/write. It proves the plugin architecture works and becomes the reference implementation for community plugin developers.
|
||
|
||
---
|
||
|
||
## 13. Build Phases
|
||
|
||
### Error Handling + Logging Strategy
|
||
|
||
**Structured logging:** Use `log/slog` (Go 1.21+ standard library) with JSON output. Log levels: DEBUG, INFO, WARN, ERROR. Log to `%APPDATA%\Wraith\wraith.log` with daily rotation (keep 7 days).
|
||
|
||
**Connection drops:** When an SSH/RDP connection drops unexpectedly:
|
||
1. Session Manager marks session as `disconnected`
|
||
2. Frontend tab shows "Connection lost — [Reconnect] [Close]"
|
||
3. Auto-reconnect is opt-in (configurable per connection via `options` JSON)
|
||
4. If auto-reconnect is enabled, retry 3 times with exponential backoff (1s, 2s, 4s)
|
||
|
||
**Error surfacing:** Errors from Go backend are emitted as Wails events with a severity level. Frontend shows:
|
||
- Transient errors (network timeout) → toast notification, auto-dismiss
|
||
- Actionable errors (auth failure) → modal with explanation and action button
|
||
- Fatal errors (vault corruption) → full-screen error with instructions
|
||
|
||
**Sensitive data in logs:** Never log passwords, private keys, or decrypted credentials. Log only: connection IDs, hostnames, session IDs, error types.
|
||
|
||
### Crash Recovery + Workspace Restore
|
||
|
||
When the app crashes, the system reboots, or Wails dies, SSH/RDP sessions are gone — there's no way to recover a dropped TCP connection. But the **workspace layout** can be restored.
|
||
|
||
**Workspace snapshots:** The Session Manager periodically writes a workspace snapshot to SQLite (every 30 seconds and on clean shutdown):
|
||
|
||
```json
|
||
{
|
||
"tabs": [
|
||
{"connectionId": 1, "protocol": "ssh", "position": 0, "detached": false},
|
||
{"connectionId": 5, "protocol": "rdp", "position": 1, "detached": false},
|
||
{"connectionId": 3, "protocol": "ssh", "position": 2, "detached": true, "windowBounds": {...}}
|
||
],
|
||
"sidebarWidth": 240,
|
||
"sidebarMode": "connections",
|
||
"activeTab": 0
|
||
}
|
||
```
|
||
|
||
**On restart after crash:**
|
||
1. Detect unclean shutdown (snapshot exists but no `clean_shutdown` flag)
|
||
2. Show: "Wraith closed unexpectedly. Restore previous workspace? [Restore] [Start Fresh]"
|
||
3. If Restore: recreate tab layout, attempt to reconnect each session
|
||
4. Tabs that fail to reconnect show "Connection lost — [Retry] [Close]"
|
||
|
||
Users care about continuity more than perfection. Even if every session dies, restoring the layout and offering one-click reconnect is a massive UX win.
|
||
|
||
### Resource Management
|
||
|
||
With 20+ SSH sessions and multiple RDP sessions, resource awareness is critical:
|
||
|
||
**Memory budget:** Each SSH session costs ~2-5MB (PTY buffer + SFTP client). Each RDP session costs ~8-12MB (pixel buffer at 1080p). Target: stable at 20 SSH + 3 RDP (~100-120MB total backend memory).
|
||
|
||
**Session limits:**
|
||
- Default max: 32 concurrent sessions (SSH + RDP combined)
|
||
- Configurable via settings
|
||
- When limit reached: "Maximum sessions reached. Close a session to open a new one."
|
||
|
||
**Inactive session handling:**
|
||
- Sessions idle for 30+ minutes get a subtle "idle" indicator on the tab (dimmed text)
|
||
- SSH keepalive (`ServerAliveInterval` equivalent) prevents server-side timeouts — configurable per connection via `options.keepAliveInterval` (default: 60 seconds)
|
||
- No automatic session suspension — users control their sessions explicitly
|
||
- SFTP idle connections are closed after 10 minutes of inactivity and silently reopened on next file operation
|
||
|
||
**Monitoring:** Expose a "Sessions" panel in Settings showing per-session memory usage, connection duration, and idle time. Simple table, not a dashboard.
|
||
|
||
---
|
||
|
||
## 14. Licensing + Open Source
|
||
|
||
**License:** MIT. All dependencies must be MIT, Apache 2.0, BSD, or ISC compatible. **No GPL/AGPL dependencies.**
|
||
|
||
Dependency license audit is part of Phase 1. Key libraries and their licenses:
|
||
- `golang.org/x/crypto` — BSD-3-Clause ✓
|
||
- `github.com/pkg/sftp` — BSD-2-Clause ✓
|
||
- `github.com/ebitengine/purego` — Apache 2.0 ✓
|
||
- `modernc.org/sqlite` — BSD-3-Clause ✓
|
||
- FreeRDP3 — Apache 2.0 ✓ (dynamically linked, no license contamination)
|
||
- xterm.js — MIT ✓
|
||
- Vue 3 — MIT ✓
|
||
- Naive UI — MIT ✓
|
||
- Tailwind CSS — MIT ✓
|
||
- CodeMirror 6 — MIT ✓
|
||
|
||
**Plugin architecture:** The Go backend exposes a plugin interface so community developers can extend Wraith with custom protocol handlers, importers, or sidebar panels. Plugins are Go packages that implement defined interfaces and are compiled into the binary (no runtime plugin loading — keeps the binary simple and portable).
|
||
|
||
**README.md:** Comprehensive developer-facing documentation covering: architecture overview, build instructions, project structure walkthrough, plugin development guide, contribution guidelines, and the design philosophy. Written as part of Phase 1.
|
||
|
||
---
|
||
|
||
## 15. First-Run Experience
|
||
|
||
On first launch:
|
||
1. Master password creation dialog (set + confirm)
|
||
2. Detect if `.mobaconf` files exist in common locations (`%APPDATA%\MobaXterm\`, user's Documents folder)
|
||
3. If found: prompt "We found a MobaXTerm configuration. Import your sessions?" with file path shown
|
||
4. If not found: offer "Import from MobaXTerm" button + "Start fresh"
|
||
5. After import (or skip): land on the empty connection manager with a "Create your first connection" prompt
|
||
|
||
---
|
||
|
||
## 16. Build Phases
|
||
|
||
| Phase | Deliverables |
|
||
|---|---|
|
||
| **1: Foundation** | Wails v3 scaffold (including multi-window spike — validate Plan A/B/C), SQLite schema + migrations (WAL mode), vault service (master password, Argon2id, AES-256-GCM), connection CRUD, group tree, Vue 3 shell with sidebar + tab container, dark theme, Naive UI integration, plugin interface definitions, README.md, license audit, **RDP frame transport spike** (benchmark HTTP vs base64 with a test canvas — don't wait until Phase 3) |
|
||
| **2: SSH + SFTP** | SSH service (x/crypto/ssh), PTY + shell, xterm.js terminal rendering, multi-tab sessions, SFTP sidebar (pkg/sftp), file tree, upload/download, CWD following (OSC 7), CodeMirror 6 editor in separate window, workspace snapshot persistence |
|
||
| **3: RDP** | FreeRDP3 purego bindings, pixel buffer, canvas rendering (using proven transport from Phase 1 spike), mouse/keyboard input mapping (including scancode table + system key pass-through), clipboard sync, connection options |
|
||
| **4: Polish** | Command palette, tab detach/reattach, terminal theming (built-in + custom), MobaXTerm importer (with first-run detection), tab badges, session context awareness, quick connect, host key management UI, settings page, crash recovery / workspace restore, resource management panel, NSIS installer |
|