diff --git a/packages/hoppscotch-agent/devenv.nix b/packages/hoppscotch-agent/devenv.nix index 7cecdfb45..27603e56f 100644 --- a/packages/hoppscotch-agent/devenv.nix +++ b/packages/hoppscotch-agent/devenv.nix @@ -1,20 +1,33 @@ { pkgs, lib, config, inputs, ... }: -{ - # https://devenv.sh/packages/ - packages = with pkgs; [ - git - openssl - postgresql_16 - jq - xxd - # BE and Tauri stuff +let + rosettaPkgs = + if pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64 + then pkgs.pkgsx86_64Darwin + else pkgs; + + darwinPackages = with pkgs; [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.CoreServices + darwin.apple_sdk.frameworks.CoreFoundation + darwin.apple_sdk.frameworks.Foundation + darwin.apple_sdk.frameworks.AppKit + darwin.apple_sdk.frameworks.WebKit + ]; + + linuxPackages = with pkgs; [ libsoup_3 webkitgtk_4_1 librsvg libappindicator libayatana-appindicator - libappindicator-gtk3 + ]; + +in { + # https://devenv.sh/packages/ + packages = with pkgs; [ + git + postgresql_16 # FE and Node stuff nodejs_22 nodePackages_latest.typescript-language-server @@ -23,15 +36,16 @@ prisma-engines # Cargo cargo-edit - ]; + ] ++ lib.optionals pkgs.stdenv.isDarwin darwinPackages + ++ lib.optionals pkgs.stdenv.isLinux linuxPackages; # https://devenv.sh/basics/ - # - # NOTE: Setting these `PRISMA_*` environment variable fixes - # Error: Failed to fetch sha256 checksum at https://binaries.prisma.sh/all_commits//linux-nixos/libquery_engine.so.node.gz.sha256 - 404 Not Found - # See: https://github.com/prisma/prisma/discussions/3120 env = { APP_GREET = "Hoppscotch"; + } // lib.optionalAttrs pkgs.stdenv.isLinux { + # NOTE: Setting these `PRISMA_*` environment variable fixes + # Error: Failed to fetch sha256 checksum at https://binaries.prisma.sh/all_commits//linux-nixos/libquery_engine.so.node.gz.sha256 - 404 Not Found + # See: https://github.com/prisma/prisma/discussions/3120 PRISMA_QUERY_ENGINE_LIBRARY = "${pkgs.prisma-engines}/lib/libquery_engine.node"; PRISMA_QUERY_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/query-engine"; PRISMA_SCHEMA_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/schema-engine"; @@ -39,15 +53,25 @@ LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.libappindicator pkgs.libayatana-appindicator - pkgs.libappindicator-gtk3 ]; + } // lib.optionalAttrs pkgs.stdenv.isDarwin { + # Place to put macOS-specific environment variables }; # https://devenv.sh/scripts/ - scripts.hello.exec = "echo hello from $APP_GREET"; + scripts = { + hello.exec = "echo hello from $APP_GREET"; + e.exec = "emacs"; + }; enterShell = '' git --version + ${lib.optionalString pkgs.stdenv.isDarwin '' + # Place to put macOS-specific shell initialization + ''} + ${lib.optionalString pkgs.stdenv.isLinux '' + # Place to put Linux-specific shell initialization + ''} ''; # https://devenv.sh/tests/ @@ -59,33 +83,29 @@ dotenv.enable = true; # https://devenv.sh/languages/ - languages.javascript = { - enable = true; - pnpm = { + languages = { + typescript.enable = true; + javascript = { enable = true; + pnpm.enable = true; + npm.enable = true; }; - npm = { + rust = { enable = true; + channel = "nightly"; + components = [ + "rustc" + "cargo" + "clippy" + "rustfmt" + "rust-analyzer" + "llvm-tools-preview" + "rust-src" + "rustc-codegen-cranelift-preview" + ]; }; }; - languages.typescript.enable = true; - - languages.rust = { - enable = true; - channel = "nightly"; - components = [ - "rustc" - "cargo" - "clippy" - "rustfmt" - "rust-analyzer" - "llvm-tools-preview" - "rust-src" - "rustc-codegen-cranelift-preview" - ]; - }; - # https://devenv.sh/pre-commit-hooks/ # pre-commit.hooks.shellcheck.enable = true; diff --git a/packages/hoppscotch-agent/package.json b/packages/hoppscotch-agent/package.json index 2dfa2fc7c..19b6f64e1 100644 --- a/packages/hoppscotch-agent/package.json +++ b/packages/hoppscotch-agent/package.json @@ -1,7 +1,7 @@ { "name": "hoppscotch-agent", "private": true, - "version": "0.1.2", + "version": "0.1.3", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/hoppscotch-agent/src-tauri/Cargo.lock b/packages/hoppscotch-agent/src-tauri/Cargo.lock index f2dd778ef..4893f4e86 100644 --- a/packages/hoppscotch-agent/src-tauri/Cargo.lock +++ b/packages/hoppscotch-agent/src-tauri/Cargo.lock @@ -2111,7 +2111,7 @@ dependencies = [ [[package]] name = "hoppscotch-agent" -version = "0.1.2" +version = "0.1.3" dependencies = [ "aes-gcm", "axum", diff --git a/packages/hoppscotch-agent/src-tauri/Cargo.toml b/packages/hoppscotch-agent/src-tauri/Cargo.toml index d4f42f3c1..79b8140ac 100644 --- a/packages/hoppscotch-agent/src-tauri/Cargo.toml +++ b/packages/hoppscotch-agent/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hoppscotch-agent" -version = "0.1.2" +version = "0.1.3" description = "A cross-platform HTTP request agent for Hoppscotch for advanced request handling including custom headers, certificates, proxies, and local system integration." authors = ["AndrewBastin", "CuriousCorrelation"] edition = "2021" diff --git a/packages/hoppscotch-agent/src-tauri/src/controller.rs b/packages/hoppscotch-agent/src-tauri/src/controller.rs index a0d081b6b..00e0006f9 100644 --- a/packages/hoppscotch-agent/src-tauri/src/controller.rs +++ b/packages/hoppscotch-agent/src-tauri/src/controller.rs @@ -14,7 +14,7 @@ use tauri::{AppHandle, Emitter}; use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::{ - error::{AppError, AppResult}, + error::{AgentError, AgentResult}, model::{AuthKeyResponse, ConfirmedRegistrationRequest, HandshakeResponse}, state::{AppState, Registration}, util::EncryptedJson, @@ -32,7 +32,7 @@ fn generate_otp() -> String { pub async fn handshake( State((_, app_handle)): State<(Arc, AppHandle)>, -) -> AppResult> { +) -> AgentResult> { Ok(Json(HandshakeResponse { status: "success".to_string(), __hoppscotch__agent__: true, @@ -42,7 +42,7 @@ pub async fn handshake( pub async fn receive_registration( State((state, app_handle)): State<(Arc, AppHandle)>, -) -> AppResult> { +) -> AgentResult> { let otp = generate_otp(); let mut active_registration_code = state.active_registration_code.write().await; @@ -57,7 +57,7 @@ pub async fn receive_registration( app_handle .emit("registration_received", otp) - .map_err(|_| AppError::InternalServerError)?; + .map_err(|_| AgentError::InternalServerError)?; Ok(Json( json!({ "message": "Registration received and stored" }), @@ -67,12 +67,12 @@ pub async fn receive_registration( pub async fn verify_registration( State((state, app_handle)): State<(Arc, AppHandle)>, Json(confirmed_registration): Json, -) -> AppResult> { +) -> AgentResult> { state .validate_registration(&confirmed_registration.registration) .await .then_some(()) - .ok_or(AppError::InvalidRegistration)?; + .ok_or(AgentError::InvalidRegistration)?; let auth_key = Uuid::new_v4().to_string(); let created_at = Utc::now(); @@ -85,9 +85,9 @@ pub async fn verify_registration( let their_public_key = { let public_key_slice: &[u8; 32] = &base16::decode(&confirmed_registration.client_public_key_b16) - .map_err(|_| AppError::InvalidClientPublicKey)?[0..32] + .map_err(|_| AgentError::InvalidClientPublicKey)?[0..32] .try_into() - .map_err(|_| AppError::InvalidClientPublicKey)?; + .map_err(|_| AgentError::InvalidClientPublicKey)?; PublicKey::from(public_key_slice.to_owned()) }; @@ -111,7 +111,7 @@ pub async fn verify_registration( app_handle .emit("authenticated", &auth_payload) - .map_err(|_| AppError::InternalServerError)?; + .map_err(|_| AgentError::InternalServerError)?; Ok(Json(AuthKeyResponse { auth_key, @@ -125,22 +125,22 @@ pub async fn run_request( TypedHeader(auth_header): TypedHeader>, headers: HeaderMap, body: Bytes, -) -> AppResult> { +) -> AgentResult> { let nonce = headers .get("X-Hopp-Nonce") - .ok_or(AppError::Unauthorized)? + .ok_or(AgentError::Unauthorized)? .to_str() - .map_err(|_| AppError::Unauthorized)?; + .map_err(|_| AgentError::Unauthorized)?; let req: RequestWithMetadata = state .validate_access_and_get_data(auth_header.token(), nonce, &body) - .ok_or(AppError::Unauthorized)?; + .ok_or(AgentError::Unauthorized)?; let req_id = req.req_id; let reg_info = state .get_registration_info(auth_header.token()) - .ok_or(AppError::Unauthorized)?; + .ok_or(AgentError::Unauthorized)?; let cancel_token = tokio_util::sync::CancellationToken::new(); state.add_cancellation_token(req.req_id, cancel_token.clone()); @@ -164,11 +164,11 @@ pub async fn run_request( res = tokio::task::spawn_blocking(move || hoppscotch_relay::run_request_task(&req, cancel_token_clone)) => { match res { Ok(task_result) => Ok(task_result?), - Err(_) => Err(AppError::InternalServerError), + Err(_) => Err(AgentError::InternalServerError), } }, _ = cancel_token.cancelled() => { - Err(AppError::RequestCancelled) + Err(AgentError::RequestCancelled) } }; @@ -190,7 +190,7 @@ pub async fn run_request( pub async fn registered_handshake( State((state, _)): State<(Arc, AppHandle)>, TypedHeader(auth_header): TypedHeader>, -) -> AppResult> { +) -> AgentResult> { let reg_info = state.get_registration_info(auth_header.token()); match reg_info { @@ -198,7 +198,7 @@ pub async fn registered_handshake( key_b16: reg.shared_secret_b16, data: json!(true), }), - None => Err(AppError::Unauthorized), + None => Err(AgentError::Unauthorized), } } @@ -206,15 +206,15 @@ pub async fn cancel_request( State((state, _app_handle)): State<(Arc, T)>, TypedHeader(auth_header): TypedHeader>, Path(req_id): Path, -) -> AppResult> { +) -> AgentResult> { if !state.validate_access(auth_header.token()) { - return Err(AppError::Unauthorized); + return Err(AgentError::Unauthorized); } if let Some((_, token)) = state.remove_cancellation_token(req_id) { token.cancel(); Ok(Json(json!({"message": "Request cancelled successfully"}))) } else { - Err(AppError::RequestNotFound) + Err(AgentError::RequestNotFound) } } diff --git a/packages/hoppscotch-agent/src-tauri/src/error.rs b/packages/hoppscotch-agent/src-tauri/src/error.rs index 8bbb16cab..697b3dd3b 100644 --- a/packages/hoppscotch-agent/src-tauri/src/error.rs +++ b/packages/hoppscotch-agent/src-tauri/src/error.rs @@ -7,7 +7,7 @@ use serde_json::json; use thiserror::Error; #[derive(Error, Debug)] -pub enum AppError { +pub enum AgentError { #[error("Invalid Registration")] InvalidRegistration, #[error("Invalid Client Public Key")] @@ -40,28 +40,30 @@ pub enum AppError { RegistrationInsertError, #[error("Failed to save registrations to store")] RegistrationSaveError, + #[error("Serde error: {0}")] + Serde(#[from] serde_json::Error), #[error("Store error: {0}")] TauriPluginStore(#[from] tauri_plugin_store::Error), #[error("Relay error: {0}")] Relay(#[from] hoppscotch_relay::RelayError), } -impl IntoResponse for AppError { +impl IntoResponse for AgentError { fn into_response(self) -> Response { let (status, error_message) = match self { - AppError::InvalidRegistration => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::InvalidClientPublicKey => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), - AppError::RequestNotFound => (StatusCode::NOT_FOUND, self.to_string()), - AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), - AppError::ClientCertError => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::RootCertError => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::InvalidMethod => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::InvalidUrl => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::InvalidHeaders => (StatusCode::BAD_REQUEST, self.to_string()), - AppError::RequestRunError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), - AppError::RequestCancelled => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::InvalidRegistration => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::InvalidClientPublicKey => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), + AgentError::RequestNotFound => (StatusCode::NOT_FOUND, self.to_string()), + AgentError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + AgentError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + AgentError::ClientCertError => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::RootCertError => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::InvalidMethod => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::InvalidUrl => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::InvalidHeaders => (StatusCode::BAD_REQUEST, self.to_string()), + AgentError::RequestRunError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), + AgentError::RequestCancelled => (StatusCode::BAD_REQUEST, self.to_string()), _ => ( StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error".to_string(), @@ -76,4 +78,4 @@ impl IntoResponse for AppError { } } -pub type AppResult = std::result::Result; +pub type AgentResult = std::result::Result; diff --git a/packages/hoppscotch-agent/src-tauri/src/global.rs b/packages/hoppscotch-agent/src-tauri/src/global.rs new file mode 100644 index 000000000..feaf7fe85 --- /dev/null +++ b/packages/hoppscotch-agent/src-tauri/src/global.rs @@ -0,0 +1,2 @@ +pub const AGENT_STORE: &str = "app_data.bin"; +pub const REGISTRATIONS: &str = "registrations"; diff --git a/packages/hoppscotch-agent/src-tauri/src/lib.rs b/packages/hoppscotch-agent/src-tauri/src/lib.rs index 997a3d6de..b8312eb0d 100644 --- a/packages/hoppscotch-agent/src-tauri/src/lib.rs +++ b/packages/hoppscotch-agent/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ pub mod controller; pub mod dialog; pub mod error; +pub mod global; pub mod model; pub mod route; pub mod server; diff --git a/packages/hoppscotch-agent/src-tauri/src/state.rs b/packages/hoppscotch-agent/src-tauri/src/state.rs index 09558b129..c8e8c928b 100644 --- a/packages/hoppscotch-agent/src-tauri/src/state.rs +++ b/packages/hoppscotch-agent/src-tauri/src/state.rs @@ -3,11 +3,14 @@ use axum::body::Bytes; use chrono::{DateTime, Utc}; use dashmap::DashMap; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use tauri_plugin_store::StoreBuilder; +use tauri_plugin_store::StoreExt; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; -use crate::error::{AppError, AppResult}; +use crate::{ + error::{AgentError, AgentResult}, + global::{AGENT_STORE, REGISTRATIONS}, +}; /// Describes one registered app instance #[derive(Debug, Clone, Serialize, Deserialize)] @@ -34,18 +37,20 @@ pub struct AppState { } impl AppState { - pub fn new(app_handle: tauri::AppHandle) -> AppResult { - let store = StoreBuilder::new(&app_handle, "app_data.bin").build()?; - - let _ = store.reload(); + pub fn new(app_handle: tauri::AppHandle) -> AgentResult { + let store = app_handle.store(AGENT_STORE)?; // Try loading and parsing registrations from the store, if that failed, // load the default list let registrations = store - .get("registrations") + .get(REGISTRATIONS) .and_then(|val| serde_json::from_value(val.clone()).ok()) .unwrap_or_else(|| DashMap::new()); + // Try to save the latest registrations list + let _ = store.set(REGISTRATIONS, serde_json::to_value(®istrations)?); + let _ = store.save(); + Ok(Self { active_registration_code: RwLock::new(None), cancellation_tokens: DashMap::new(), @@ -62,38 +67,50 @@ impl AppState { } /// Provides you an opportunity to update the registrations list - /// and also persists the data to the disk + /// and also persists the data to the disk. + /// This function bypasses `store.reload()` to avoid issues from stale or inconsistent + /// data on disk. By relying solely on the in-memory `self.registrations`, + /// we make sure that updates are applied based on the most recent changes in memory. pub fn update_registrations( &self, app_handle: tauri::AppHandle, update_func: impl FnOnce(&DashMap), - ) -> Result<(), AppError> { + ) -> Result<(), AgentError> { update_func(&self.registrations); - let store = StoreBuilder::new(&app_handle, "app_data.bin").build()?; + let store = app_handle.store(AGENT_STORE)?; - let _ = store.reload()?; + if store.has(REGISTRATIONS) { + // We've confirmed `REGISTRATIONS` exists in the store + store + .delete(REGISTRATIONS) + .then_some(()) + .ok_or(AgentError::RegistrationClearError)?; + } else { + log::debug!("`REGISTRATIONS` key not found in store; continuing with update."); + } - let _ = store - .delete("registrations") - .then_some(()) - .ok_or(AppError::RegistrationClearError)?; + // Since we've established `self.registrations` as the source of truth, + // we avoid reloading the store from disk and instead choose to override it. - let _ = store.set( - "registrations", - serde_json::to_value(self.registrations.clone()).unwrap(), + store.set( + REGISTRATIONS, + serde_json::to_value(self.registrations.clone())?, ); - store.save().map_err(|_| AppError::RegistrationSaveError)?; + // Explicitly save the changes + store.save()?; Ok(()) } + /// Clear all the registrations + pub fn clear_registrations(&self, app_handle: tauri::AppHandle) -> Result<(), AgentError> { + Ok(self.update_registrations(app_handle, |registrations| registrations.clear())?) + } + pub async fn validate_registration(&self, registration: &str) -> bool { - match *self.active_registration_code.read().await { - Some(ref code) => code == registration, - None => false, - } + self.active_registration_code.read().await.as_deref() == Some(registration) } pub fn remove_cancellation_token(&self, req_id: usize) -> Option<(usize, CancellationToken)> { diff --git a/packages/hoppscotch-agent/src-tauri/src/tray.rs b/packages/hoppscotch-agent/src-tauri/src/tray.rs index 1202c7010..1b1f62e81 100644 --- a/packages/hoppscotch-agent/src-tauri/src/tray.rs +++ b/packages/hoppscotch-agent/src-tauri/src/tray.rs @@ -57,18 +57,19 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { .menu_on_left_click(true) .on_menu_event(move |app, event| match event.id.as_ref() { "quit" => { + log::info!("Exiting the agent..."); app.exit(-1); } "clear_registrations" => { let app_state = app.state::>(); app_state - .update_registrations(app.clone(), |regs| { - regs.clear(); - }) - .expect("Failed to clear registrations"); + .clear_registrations(app.clone()) + .expect("Invariant violation: Failed to clear registrations"); + } + _ => { + log::warn!("Unhandled menu event: {:?}", event.id); } - _ => {} }) .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { diff --git a/packages/hoppscotch-agent/src-tauri/tauri.conf.json b/packages/hoppscotch-agent/src-tauri/tauri.conf.json index 48729e399..13dfb3d02 100644 --- a/packages/hoppscotch-agent/src-tauri/tauri.conf.json +++ b/packages/hoppscotch-agent/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2.0.0-rc", "productName": "Hoppscotch Agent", - "version": "0.1.2", + "version": "0.1.3", "identifier": "io.hoppscotch.agent", "build": { "beforeDevCommand": "pnpm dev", diff --git a/packages/hoppscotch-agent/src-tauri/tauri.portable.conf.json b/packages/hoppscotch-agent/src-tauri/tauri.portable.conf.json index 9c164cc03..1797f52de 100644 --- a/packages/hoppscotch-agent/src-tauri/tauri.portable.conf.json +++ b/packages/hoppscotch-agent/src-tauri/tauri.portable.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2.0.0-rc", "productName": "Hoppscotch Agent Portable", - "version": "0.1.2", + "version": "0.1.3", "identifier": "io.hoppscotch.agent", "build": { "beforeDevCommand": "pnpm dev",