(pluginName: string, allowScopedPackageNames = false)
| 63 | * @param pluginName - The npm package name of the plugin to install |
| 64 | */ |
| 65 | export default async function installPlugin(pluginName: string, allowScopedPackageNames = false): Promise<void> { |
| 66 | const validationError = validatePluginName(pluginName, allowScopedPackageNames); |
| 67 | |
| 68 | if (validationError) { |
| 69 | throw new Error(validationError); |
| 70 | } |
| 71 | |
| 72 | let tmpDir = ''; |
| 73 | |
| 74 | try { |
| 75 | // Step 1: Validate the plugin and fetch its npm metadata |
| 76 | const info: InsomniaPlugin = await getPluginInfo(pluginName, allowScopedPackageNames); |
| 77 | |
| 78 | // Get the normalized module name (without version suffixes) |
| 79 | const moduleName = info.name; |
| 80 | |
| 81 | // Check the module name for any invalid characters |
| 82 | // This is a basic validation to ensure the module name is safe |
| 83 | // and doesn't contain any unexpected characters. |
| 84 | const validationError = validatePluginName(moduleName, allowScopedPackageNames); |
| 85 | |
| 86 | if (validationError) { |
| 87 | throw new Error(validationError); |
| 88 | } |
| 89 | |
| 90 | // Determine the target plugin installation directory |
| 91 | const userDataPath = process.env['INSOMNIA_DATA_PATH'] || app.getPath('userData'); |
| 92 | const pluginDir = path.resolve(userDataPath, 'plugins', moduleName); |
| 93 | |
| 94 | console.log(`[plugins] Installing plugin ${moduleName} to ${pluginDir}`); |
| 95 | |
| 96 | // Step 2: Create the plugin directory if it doesn't exist |
| 97 | await mkdir(pluginDir, { recursive: true }); |
| 98 | |
| 99 | if (!info.dist?.tarball) { |
| 100 | throw new Error('Invalid plugin metadata: missing tarball URL'); |
| 101 | } |
| 102 | |
| 103 | // Step 3: only allow tarballs from known hosts |
| 104 | let tarballUrl: URL; |
| 105 | try { |
| 106 | tarballUrl = new URL(info.dist.tarball); |
| 107 | } catch { |
| 108 | throw new Error(`Invalid tarball URL in plugin metadata: ${info.dist.tarball}`); |
| 109 | } |
| 110 | const allowedTarballHostnames = await getAllowedTarballHostnames(); |
| 111 | if (!allowedTarballHostnames.includes(tarballUrl.hostname)) { |
| 112 | throw new Error(`Tarball must come from an allowed host. Got: ${tarballUrl.hostname}`); |
| 113 | } |
| 114 | // and require https, unless it's the user's own http registry (same host:port) |
| 115 | const registryUrl = new URL(await getRegistryUrl()); |
| 116 | const isUsersHttpRegistry = |
| 117 | registryUrl.protocol === 'http:' && tarballUrl.protocol === 'http:' && tarballUrl.host === registryUrl.host; |
| 118 | if (tarballUrl.protocol !== 'https:' && !isUsersHttpRegistry) { |
| 119 | throw new Error(`Tarball must be served over https. Got: ${info.dist.tarball}`); |
| 120 | } |
| 121 | |
| 122 | // Step 4: Install the plugin into a temporary directory |
no test coverage detected