Add support for SSE extensions via scheme URI (#2073)
This commit is contained in:
+10
-1
@@ -79,6 +79,12 @@ export default function App() {
|
||||
return `${cmd} ${args.join(' ')}`.trim();
|
||||
}
|
||||
|
||||
// Utility function to extract the remote url from the link
|
||||
function extractRemoteUrl(link: string): string {
|
||||
const url = new URL(link);
|
||||
return url.searchParams.get('url');
|
||||
}
|
||||
|
||||
const setView = (view: View, viewOptions: ViewOptions = {}) => {
|
||||
console.log(`Setting view to: ${view}`, viewOptions);
|
||||
setInternalView({ view, viewOptions });
|
||||
@@ -257,11 +263,14 @@ export default function App() {
|
||||
try {
|
||||
console.log(`Received add-extension event with link: ${link}`);
|
||||
const command = extractCommand(link);
|
||||
const remoteUrl = extractRemoteUrl(link);
|
||||
const extName = extractExtensionName(link);
|
||||
window.electron.logInfo(`Adding extension from deep link ${link}`);
|
||||
setPendingLink(link);
|
||||
|
||||
const messageDetails = remoteUrl ? `Remote URL: ${remoteUrl}` : `Command: ${command}`;
|
||||
setModalMessage(
|
||||
`Are you sure you want to install the ${extName} extension?\n\nCommand: ${command}`
|
||||
`Are you sure you want to install the ${extName} extension?\n\n${messageDetails}`
|
||||
);
|
||||
setModalVisible(true);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,75 @@ import { toastService } from '../../../toasts';
|
||||
import { activateExtension } from './extension-manager';
|
||||
import { DEFAULT_EXTENSION_TIMEOUT, nameToKey } from './utils';
|
||||
|
||||
/**
|
||||
* Build an extension config for stdio from the deeplink URL
|
||||
*/
|
||||
function getStdioConfig(
|
||||
cmd: string,
|
||||
parsedUrl: URL,
|
||||
name: string,
|
||||
description: string,
|
||||
timeout: number
|
||||
) {
|
||||
// Validate that the command is one of the allowed commands
|
||||
const allowedCommands = ['jbang', 'npx', 'uvx', 'goosed'];
|
||||
if (!allowedCommands.includes(cmd)) {
|
||||
toastService.handleError(
|
||||
'Invalid Command',
|
||||
`Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,
|
||||
{ shouldThrow: true }
|
||||
);
|
||||
}
|
||||
|
||||
// Check for security risk with npx -c command
|
||||
const args = parsedUrl.searchParams.getAll('arg');
|
||||
if (cmd === 'npx' && args.includes('-c')) {
|
||||
toastService.handleError(
|
||||
'Security Risk',
|
||||
'Failed to install extension: npx with -c argument can lead to code injection',
|
||||
{ shouldThrow: true }
|
||||
);
|
||||
}
|
||||
|
||||
const envList = parsedUrl.searchParams.getAll('env');
|
||||
|
||||
// Create the extension config
|
||||
const config: ExtensionConfig = {
|
||||
name: name,
|
||||
type: 'stdio',
|
||||
cmd: cmd,
|
||||
description,
|
||||
args: args,
|
||||
envs:
|
||||
envList.length > 0
|
||||
? Object.fromEntries(
|
||||
envList.map((env) => {
|
||||
const [key] = env.split('=');
|
||||
return [key, '']; // Initialize with empty string as value
|
||||
})
|
||||
)
|
||||
: undefined,
|
||||
timeout: timeout,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an extension config for SSE from the deeplink URL
|
||||
*/
|
||||
function getSseConfig(remoteUrl: string, name: string, description: string, timeout: number) {
|
||||
const config: ExtensionConfig = {
|
||||
name,
|
||||
type: 'sse',
|
||||
uri: remoteUrl,
|
||||
description,
|
||||
timeout: timeout,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding an extension from a deeplink URL
|
||||
*/
|
||||
@@ -39,56 +108,17 @@ export async function addExtensionFromDeepLink(
|
||||
}
|
||||
}
|
||||
|
||||
const cmd = parsedUrl.searchParams.get('cmd');
|
||||
if (!cmd) {
|
||||
toastService.handleError(
|
||||
'Missing Command',
|
||||
"Failed to install extension: Missing required 'cmd' parameter in the URL",
|
||||
{ shouldThrow: true }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that the command is one of the allowed commands
|
||||
const allowedCommands = ['jbang', 'npx', 'uvx', 'goosed'];
|
||||
if (!allowedCommands.includes(cmd)) {
|
||||
toastService.handleError(
|
||||
'Invalid Command',
|
||||
`Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,
|
||||
{ shouldThrow: true }
|
||||
);
|
||||
}
|
||||
|
||||
// Check for security risk with npx -c command
|
||||
const args = parsedUrl.searchParams.getAll('arg');
|
||||
if (cmd === 'npx' && args.includes('-c')) {
|
||||
toastService.handleError(
|
||||
'Security Risk',
|
||||
'Failed to install extension: npx with -c argument can lead to code injection',
|
||||
{ shouldThrow: true }
|
||||
);
|
||||
}
|
||||
|
||||
const envList = parsedUrl.searchParams.getAll('env');
|
||||
const name = parsedUrl.searchParams.get('name')!;
|
||||
const timeout = parsedUrl.searchParams.get('timeout');
|
||||
const parsedTimeout = parsedUrl.searchParams.get('timeout');
|
||||
const timeout = parsedTimeout ? parseInt(parsedTimeout, 10) : DEFAULT_EXTENSION_TIMEOUT;
|
||||
const description = parsedUrl.searchParams.get('description');
|
||||
|
||||
// Create the extension config
|
||||
const config: ExtensionConfig = {
|
||||
name: name,
|
||||
type: 'stdio',
|
||||
cmd: cmd,
|
||||
args: args,
|
||||
envs:
|
||||
envList.length > 0
|
||||
? Object.fromEntries(
|
||||
envList.map((env) => {
|
||||
const [key] = env.split('=');
|
||||
return [key, '']; // Initialize with empty string as value
|
||||
})
|
||||
)
|
||||
: undefined,
|
||||
timeout: timeout ? parseInt(timeout, 10) : DEFAULT_EXTENSION_TIMEOUT,
|
||||
};
|
||||
const cmd = parsedUrl.searchParams.get('cmd');
|
||||
const remoteUrl = parsedUrl.searchParams.get('url');
|
||||
|
||||
const config = remoteUrl
|
||||
? getSseConfig(remoteUrl, name, description, timeout)
|
||||
: getStdioConfig(cmd!, parsedUrl, name, description, timeout);
|
||||
|
||||
// Check if extension requires env vars and go to settings if so
|
||||
if (config.envs && Object.keys(config.envs).length > 0) {
|
||||
|
||||
@@ -295,11 +295,50 @@ export async function addExtensionFromDeepLink(
|
||||
}
|
||||
|
||||
const cmd = parsedUrl.searchParams.get('cmd');
|
||||
if (!cmd) {
|
||||
handleError("Failed to install extension: Missing required 'cmd' parameter in the URL", true);
|
||||
const remoteUrl = parsedUrl.searchParams.get('url');
|
||||
|
||||
if (!cmd && !remoteUrl) {
|
||||
handleError(
|
||||
"Failed to install extension: Missing required 'cmd' or 'url' parameter in the URL",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that the command is one of the allowed commands
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
const name = parsedUrl.searchParams.get('name');
|
||||
const description = parsedUrl.searchParams.get('description');
|
||||
const timeout = parsedUrl.searchParams.get('timeout');
|
||||
|
||||
// Create a ExtensionConfig from the URL parameters
|
||||
// Parse timeout if provided, otherwise use default
|
||||
const parsedTimeout = timeout ? parseInt(timeout, 10) : null;
|
||||
|
||||
const config: FullExtensionConfig = cmd
|
||||
? getStdioConfig(cmd, parsedUrl, id, name, description, parsedTimeout)
|
||||
: getSseConfig(remoteUrl, id, name, description, parsedTimeout);
|
||||
|
||||
// Store the extension config regardless of env vars status
|
||||
storeExtensionConfig(config);
|
||||
|
||||
// Check if extension requires env vars and go to settings if so
|
||||
if (envVarsRequired(config)) {
|
||||
console.log('Environment variables required, redirecting to settings');
|
||||
setView('settings', { extensionId: config.id, showEnvVars: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// If no env vars are required, proceed with extending Goosed
|
||||
await addExtension(config);
|
||||
}
|
||||
|
||||
function getStdioConfig(
|
||||
cmd: string,
|
||||
parsedUrl: URL,
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
parsedTimeout: number
|
||||
) {
|
||||
const allowedCommands = ['jbang', 'npx', 'uvx', 'goosed'];
|
||||
if (!allowedCommands.includes(cmd)) {
|
||||
handleError(
|
||||
@@ -315,10 +354,6 @@ export async function addExtensionFromDeepLink(
|
||||
}
|
||||
|
||||
const envList = parsedUrl.searchParams.getAll('env');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
const name = parsedUrl.searchParams.get('name');
|
||||
const description = parsedUrl.searchParams.get('description');
|
||||
const timeout = parsedUrl.searchParams.get('timeout');
|
||||
|
||||
// split env based on delimiter to a map
|
||||
const envs = envList.reduce(
|
||||
@@ -330,10 +365,6 @@ export async function addExtensionFromDeepLink(
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
// Create a ExtensionConfig from the URL parameters
|
||||
// Parse timeout if provided, otherwise use default
|
||||
const parsedTimeout = timeout ? parseInt(timeout, 10) : null;
|
||||
|
||||
const config: FullExtensionConfig = {
|
||||
id,
|
||||
name,
|
||||
@@ -349,16 +380,29 @@ export async function addExtensionFromDeepLink(
|
||||
: DEFAULT_EXTENSION_TIMEOUT,
|
||||
};
|
||||
|
||||
// Store the extension config regardless of env vars status
|
||||
storeExtensionConfig(config);
|
||||
|
||||
// Check if extension requires env vars and go to settings if so
|
||||
if (envVarsRequired(config)) {
|
||||
console.log('Environment variables required, redirecting to settings');
|
||||
setView('settings', { extensionId: config.id, showEnvVars: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// If no env vars are required, proceed with extending Goosed
|
||||
await addExtension(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
function getSseConfig(
|
||||
remoteUrl: string,
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
parsedTimeout: number
|
||||
) {
|
||||
const config: FullExtensionConfig = {
|
||||
id,
|
||||
name,
|
||||
type: 'sse',
|
||||
uri: remoteUrl,
|
||||
description,
|
||||
enabled: true,
|
||||
env_keys: [],
|
||||
timeout:
|
||||
parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout)
|
||||
? parsedTimeout
|
||||
: DEFAULT_EXTENSION_TIMEOUT,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user