2
Arsitektur Sistem dan Batas Modul
Termimus tersusun dari tiga lapisan yang berbeda bahasa dan tanggung jawab: frontend React 19 + TypeScript + Vite dengan Zustand dan @xterm/xterm, backend Rust di dalam proses Tauri v2, serta relay sinkronisasi Go yang berdiri sendiri di server/. AGENTS.md:9-221
Lapisan frontend mengenal satu jalur keluar saja menuju backend, yaitu modul pembungkus di src/lib/api.ts. Setiap komponen dan store berhenti di sana, sehingga tidak ada komponen yang memanggil invoke secara langsung. AGENTS.md:9-221
Sembilan modul backend Rust
Akar crate src-tauri/src/lib.rs mendeklarasikan tepat sembilan modul: biometric, commands, db, pty, sftp, ssh, sshkey, tunnel, dan vault. src-tauri/src/lib.rs:1-92
Fungsi run() membuat masing-masing manager sebagai Arc lalu merakitnya menjadi satu AppState: Database, VaultManager, SessionManager, SftpManager, TunnelManager, dan PtyManager. src-tauri/src/lib.rs:22-303 Struct AppState di commands/mod.rs memegang keenam field Arc yang sama, sehingga setiap handler #[tauri::command] menerima State<AppState> dan tidak pernah membuat koneksi sendiri. src-tauri/src/commands/mod.rs:16-234
| Modul | Struct utama | Peran | Rujukan |
|---|---|---|---|
ssh | SessionManager, SshClientHandler | sesi SSH, streaming PTY, verifikasi host key | src-tauri/src/ssh/mod.rs:52-565 |
sftp | SftpManager | operasi SFTP jarak jauh dan helper filesystem lokal | src-tauri/src/sftp/mod.rs:356 |
tunnel | TunnelManager | port forwarding lokal | src-tauri/src/tunnel/mod.rs:197 |
vault | VaultManager | AES-256-GCM dan derivasi kunci Argon2id | src-tauri/src/vault/mod.rs:12-148 |
db | Database | SQLite lewat rusqlite, satu Mutex<Connection> | src-tauri/src/db/mod.rs:8-109 |
commands | AppState | seluruh handler #[tauri::command] | src-tauri/src/commands/mod.rs:16-234 |
pty | PtyManager | sesi shell lokal, terpisah dari SSH | src-tauri/src/pty/mod.rs:2210 |
sshkey | GeneratedKeyPair | pembuatan dan derivasi pasangan kunci | src-tauri/src/sshkey/mod.rs:3011 |
VaultManager hanya menyimpan derived_key: RwLock<Option<[u8; 32]>> di memori; tidak ada field yang menuliskan kunci ke disk. src-tauri/src/vault/mod.rs:12-148 Database membungkus satu rusqlite::Connection di dalam Mutex dan menjalankan init_tables() saat dibuat, sehingga schema dibuat sekali di awal proses. src-tauri/src/db/mod.rs:8-109 Rincian isi tabel dan kolomnya ada di Skema Database Lokal, sedangkan siklus kunci dan verifier ada di Vault dan Model Kriptografi.
graph TD
UI["src/components"] --> STORE["src/stores Zustand"]
STORE --> API["src/lib/api.ts"]
API -->|"invoke"| CMD["commands/mod.rs"]
CMD --> STATE["AppState"]
STATE --> DB["db::Database"]
STATE --> VAULT["vault::VaultManager"]
STATE --> SSH["ssh::SessionManager"]
STATE --> SFTP["sftp::SftpManager"]
STATE --> TUN["tunnel::TunnelManager"]
STATE --> PTY["pty::PtyManager"]
SSH --> HOSTKEY["SshClientHandler"]
HOSTKEY --> DB
CMD -->|"sync_push / sync_pull"| RELAY["server handlers"]
RELAY --> HUB["hub"]
RELAY --> SDB["server db"]
style DB fill:#336791,color:#fff
style SDB fill:#336791,color:#fff
style API fill:#8b5cf6,color:#fff
style STATE fill:#8b5cf6,color:#fff
style VAULT fill:#f59e0b,color:#000
style HOSTKEY fill:#2d7d46,color:#fff
%% fig: Arah dependensi dari komponen sampai storage
%% desc: Komponen React memakai store Zustand, store memanggil api.ts, dan api.ts mengirim invoke ke handler yang berbagi satu AppState berisi enam manager.Satu pintu di frontend
src/lib/api.ts dimulai dari satu impor invoke dan mengekspor satu objek api yang memetakan setiap command Tauri ke fungsi TypeScript bertipe. src/lib/api.ts:112 src/lib/api.ts:202-20413 Objek itu dikelompokkan per domain lewat komentar: Vault, Hosts, Folders, SSH Sessions, Local PTY Sessions, SSH Key Utilities, Keychain, SFTP Remote, Local Filesystem, Port Forwarding / Tunnels, Snippets, Workspaces & Layout Presets, Ping / Latency, Known Hosts / MITM Verification, Backup & Restore, dan Sync Relay. src/lib/api.ts:202-39614
Panggilan relay sinkronisasi juga melewati jalur ini: sync_test_connection, sync_push, sync_pull, dan sync_get_devices diimplementasikan di sisi Rust, bukan fetch di browser, untuk menghindari blokir mixed-content dan CORS WebKitGTK. src/lib/api.ts:365-39615 Perilaku sinkronisasinya dijelaskan di Backup, Sync Relay, dan API Server.
Setiap command wajib terdaftar di makro generate_handler![...] pada run(); daftar itu dibuka oleh commands::vault_status dan ditutup oleh commands::sync_get_devices. src-tauri/src/lib.rs:37-11516 Karena registrasi terpusat di satu tempat, command yang lupa didaftarkan tetap dapat dikompilasi tetapi tidak bisa dipanggil dari frontend. Pendaftaran tiga langkah ini adalah konvensi yang mengikat seluruh modul. AGENTS.md:24-3417
⚠️ Command baru memerlukan tiga perubahan sekaligus: fungsi
#[tauri::command]dicommands/mod.rs, entri digenerate_handler![...]padalib.rs, dan wrapper bertipe disrc/lib/api.ts. Melewatkan salah satunya membuat command diam-diam tidak dapat dipanggil. AGENTS.md:24-3417
Saat proses diminta keluar, run() mengirim pemutusan SSH untuk setiap sesi hidup lewat ssh.disconnect_all() di dalam tokio::time::timeout berdurasi 2 detik, sehingga keluar aplikasi tidak menggantung pada socket mati. src-tauri/src/lib.rs:118-13218 Urutan handshake dan pemutusan satu sesi dibahas di Siklus Hidup Koneksi SSH.
Relay Go di server/internal/
Relay sinkronisasi adalah program Go terpisah dengan lima paket internal: config, db, hub, handlers, dan middleware. main() memuat config.Load(), membuka SQLite lewat db.Open(cfg.DBPath), membuat hub.New(), lalu menyerahkan keduanya ke handlers.New dan middleware.NewAuth. server/cmd/server/main.go:25-4019
Routing memakai http.ServeMux biasa. Hanya /health yang publik; empat endpoint sync dibungkus auth.Wrap, dan seluruh mux akhirnya dibungkus middleware.CORS. server/cmd/server/main.go:48-7020
| Endpoint | Method | Middleware | Rujukan |
|---|---|---|---|
/health | GET | tanpa auth | server/cmd/server/main.go:5221 |
/api/v1/sync/status | GET | auth.Wrap | server/cmd/server/main.go:5522 |
/api/v1/sync/bundle | GET, POST | auth.Wrap | server/cmd/server/main.go:56-6523 |
/api/v1/sync/devices | GET | auth.Wrap | server/cmd/server/main.go:6624 |
/api/v1/sync/ws | GET | auth.Wrap | server/cmd/server/main.go:6725 |
/api/v1/sync/bundle menangani dua method di satu handler: http.MethodGet memanggil h.GetBundle dan http.MethodPost memanggil h.PostBundle, selain itu dijawab 405 Method not allowed. server/cmd/server/main.go:56-6523
Bila TERMIMUS_AUTH_TOKEN kosong, server mencatat peringatan [Security Warning] TERMIMUS_AUTH_TOKEN is not set! Running in OPEN mode. dan berjalan tanpa autentikasi. server/cmd/server/main.go:42-4626
graph LR
CFG["config.Load"] --> MAIN["cmd/server/main.go"]
MAIN --> DBI["db.Open"]
MAIN --> HUBI["hub.New"]
MAIN --> HANDLERS["handlers.New"]
MAIN --> AUTH["middleware.NewAuth"]
AUTH -->|"auth.Wrap"| ROUTES["http.ServeMux"]
ROUTES --> CORS["middleware.CORS"]
DBI --> SQLITE["SQLite"]
style DBI fill:#336791,color:#fff
style SQLITE fill:#336791,color:#fff
style CFG fill:#f59e0b,color:#000
style AUTH fill:#f59e0b,color:#000
style HUBI fill:#8b5cf6,color:#fff
%% fig: Perakitan relay Go
%% desc: main.go memuat config, membuka database, membuat hub, lalu membungkus setiap endpoint sync dengan auth.Wrap sebelum CORS.Batas yang menjaga dependensi satu arah
Dependensi selalu menuju satu arah: komponen → store → src/lib/api.ts → commands → manager di AppState → db. Tidak ada manager yang memanggil commands, dan tidak ada store yang menyentuh rusqlite atau russh. Satu-satunya jalur balik adalah event Tauri seperti ssh-data-${sessionId} yang dipancarkan backend ke frontend. AGENTS.md:24-3417
Batas antar bahasa juga tegas: relay Go hanya berbicara lewat HTTP dan WebSocket ke sync_* di sisi Rust, bukan ke api.ts langsung. server/cmd/server/main.go:48-7020 src/lib/api.ts:365-39615
Verifikasi batas ini dilakukan dengan dua perintah: bun run build untuk tsc + vite build di frontend, dan cargo check di dalam src-tauri untuk backend. Belum ada test suite, sehingga verifikasi manual lewat bun run tauri dev masih menjadi norma. AGENTS.md:48-5327