@@ -1,7 +1,9 @@
mod download_file_capability;
+mod npm_install_package_capability;
mod process_exec_capability;
pub use download_file_capability::*;
+pub use npm_install_package_capability::*;
pub use process_exec_capability::*;
use serde::{Deserialize, Serialize};
@@ -13,4 +15,6 @@ pub enum ExtensionCapability {
#[serde(rename = "process:exec")]
ProcessExec(ProcessExecCapability),
DownloadFile(DownloadFileCapability),
+ #[serde(rename = "npm:install")]
+ NpmInstallPackage(NpmInstallPackageCapability),
}
@@ -0,0 +1,39 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub struct NpmInstallPackageCapability {
+ pub package: String,
+}
+
+impl NpmInstallPackageCapability {
+ /// Returns whether the capability allows installing the given NPM package.
+ pub fn allows(&self, package: &str) -> bool {
+ self.package == "*" || self.package == package
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use pretty_assertions::assert_eq;
+
+ use super::*;
+
+ #[test]
+ fn test_allows() {
+ let capability = NpmInstallPackageCapability {
+ package: "*".to_string(),
+ };
+ assert_eq!(capability.allows("package"), true);
+
+ let capability = NpmInstallPackageCapability {
+ package: "react".to_string(),
+ };
+ assert_eq!(capability.allows("react"), true);
+
+ let capability = NpmInstallPackageCapability {
+ package: "react".to_string(),
+ };
+ assert_eq!(capability.allows("malicious-package"), false);
+ }
+}
@@ -63,6 +63,24 @@ impl CapabilityGranter {
Ok(())
}
+
+ pub fn grant_npm_install_package(&self, package_name: &str) -> Result<()> {
+ let is_allowed = self
+ .granted_capabilities
+ .iter()
+ .any(|capability| match capability {
+ ExtensionCapability::NpmInstallPackage(capability) => {
+ capability.allows(package_name)
+ }
+ _ => false,
+ });
+
+ if !is_allowed {
+ bail!("capability for npm:install {package_name} is not granted by the extension host",);
+ }
+
+ Ok(())
+ }
}
#[cfg(test)]
@@ -8,8 +8,8 @@ use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest};
use extension::{
CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
DebugTaskDefinition, DownloadFileCapability, ExtensionCapability, ExtensionHostProxy,
- KeyValueStoreDelegate, ProcessExecCapability, ProjectDelegate, SlashCommand,
- SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
+ KeyValueStoreDelegate, NpmInstallPackageCapability, ProcessExecCapability, ProjectDelegate,
+ SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
};
use fs::{Fs, normalize_path};
use futures::future::LocalBoxFuture;
@@ -585,6 +585,9 @@ impl WasmHost {
host: "*".to_string(),
path: vec!["**".to_string()],
}),
+ ExtensionCapability::NpmInstallPackage(NpmInstallPackageCapability {
+ package: "*".to_string(),
+ }),
],
_main_thread_message_task: task,
main_thread_message_tx: tx,
@@ -745,6 +745,9 @@ impl nodejs::Host for WasmState {
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
+ self.capability_granter
+ .grant_npm_install_package(&package_name)?;
+
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])