Browse Source

fix(agent): propagate default values to persistent store (#4511)

Shreyas 4 months ago
parent
commit
ecf5d9f6d2

+ 58 - 38
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/<hash>/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/<hash>/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;
 

+ 1 - 1
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",

+ 1 - 1
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",

+ 1 - 1
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"

+ 21 - 21
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<AppState>, AppHandle)>,
-) -> AppResult<Json<HandshakeResponse>> {
+) -> AgentResult<Json<HandshakeResponse>> {
     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<AppState>, AppHandle)>,
-) -> AppResult<Json<serde_json::Value>> {
+) -> AgentResult<Json<serde_json::Value>> {
     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<AppState>, AppHandle)>,
     Json(confirmed_registration): Json<ConfirmedRegistrationRequest>,
-) -> AppResult<Json<AuthKeyResponse>> {
+) -> AgentResult<Json<AuthKeyResponse>> {
     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<T>(
     TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
     headers: HeaderMap,
     body: Bytes,
-) -> AppResult<EncryptedJson<ResponseWithMetadata>> {
+) -> AgentResult<EncryptedJson<ResponseWithMetadata>> {
     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<T>(
         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<T>(
 pub async fn registered_handshake(
     State((state, _)): State<(Arc<AppState>, AppHandle)>,
     TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
-) -> AppResult<EncryptedJson<serde_json::Value>> {
+) -> AgentResult<EncryptedJson<serde_json::Value>> {
     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<T>(
     State((state, _app_handle)): State<(Arc<AppState>, T)>,
     TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
     Path(req_id): Path<usize>,
-) -> AppResult<Json<serde_json::Value>> {
+) -> AgentResult<Json<serde_json::Value>> {
     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)
     }
 }

+ 18 - 16
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<T> = std::result::Result<T, AppError>;
+pub type AgentResult<T> = std::result::Result<T, AgentError>;

+ 2 - 0
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";

+ 1 - 0
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;

+ 40 - 23
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<Self> {
-        let store = StoreBuilder::new(&app_handle, "app_data.bin").build()?;
-
-        let _ = store.reload();
+    pub fn new(app_handle: tauri::AppHandle) -> AgentResult<Self> {
+        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(&registrations)?);
+        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<String, Registration>),
-    ) -> 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)> {

+ 6 - 5
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::<Arc<AppState>>();
 
                 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 {

Some files were not shown because too many files changed in this diff