diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index cd13ee1e..2c7b1e1d 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -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) { diff --git a/ui/desktop/src/components/settings_v2/extensions/deeplink.ts b/ui/desktop/src/components/settings_v2/extensions/deeplink.ts index 726e50de..13edd5ab 100644 --- a/ui/desktop/src/components/settings_v2/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings_v2/extensions/deeplink.ts @@ -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) { diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx index 67cdeb40..305048fe 100644 --- a/ui/desktop/src/extensions.tsx +++ b/ui/desktop/src/extensions.tsx @@ -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 ); - // 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; }