Skip to content

Latest commit

 

History

History
560 lines (407 loc) · 26.8 KB

File metadata and controls

560 lines (407 loc) · 26.8 KB

Configuration Dialog Integration Guide

This guide explains how IDEs can integrate with the Snyk Language Server's configuration dialog to provide a user-friendly interface for managing Snyk settings.

Overview

The configuration dialog is an HTML-based interface that allows users to view and modify all Snyk Language Server settings. The dialog is triggered via an LSP command and displayed in the IDE's webview or browser component.

Architecture

The configuration dialog follows a client-server pattern:

  • Language Server: Generates HTML content with current settings, handles configuration updates
  • IDE Client: Displays the HTML, injects JavaScript functions for user interactions, applies configuration changes

JavaScript Architecture

The dialog uses a modular JavaScript architecture under infrastructure/configuration/template/js/ (loaded via app.js):

Path Role
app.js Bootstraps ConfigApp and loads modules
core/utils.js Utilities (debounce, cloning, IE-oriented helpers)
core/dom.js DOM helpers (ConfigApp.dom: get, addEvent, triggerEvent, …)
state/dirty-tracker.js DirtyTracker — dirty state vs last-saved snapshot
state/form-state.js Listeners for dirty + auto-save orchestration
ui/form-handler.js collectData() — builds the JSON object sent to the IDE (pflag-style keys, see below)
ui/reset-handler.js Section reset actions
ui/tabs.js Tab switching, folder dropdown, and no-folder disabled state
ui/tooltips.js Tooltip behavior
features/auto-save.js Validates, stringifies, calls ideBridge.saveConfig
features/validation.js Endpoint, risk score, additional env validation
features/authentication.js Auth UI wiring
features/auth-field-monitor.js Token / auth-sensitive field baseline sync
features/folders.js Folder-specific UI helpers
ide/bridge.js ConfigApp.ideBridge — save, login/logout via executeCommand, dirty notify

Global instances: window.dirtyTracker, window.ConfigApp (namespace). IDE bridge functions are on window (see below).

Integration Flow

1. Opening the Configuration Dialog

The IDE triggers the configuration dialog by executing the LSP command snyk.workspace.configuration.

Command Details:

  • Command: workspace/executeCommand
  • Command ID: snyk.workspace.configuration
  • Arguments: [] (no arguments required)

Response:

"<html>... full HTML content ...</html>"

The command returns the complete HTML content as a string. The IDE can directly display this in a webview without any additional processing.

See Opening Configuration Dialog Sequence for the detailed flow.

2. Displaying the HTML Content

The IDE should:

  1. Execute the command and receive the response
  2. Use the returned string as the webview HTML (the handler returns the HTML body directly)
  3. Create a webview or browser component
  4. Inject IDE-specific JavaScript functions (see Function Injection)
  5. Load the HTML content and display it

Example (conceptual):

// Execute command and receive HTML
const html = await client.sendRequest('workspace/executeCommand', {
  command: 'snyk.workspace.configuration',
  arguments: []
});

// Create webview and display
const webview = createWebview();
webview.html = injectFunctions(html);
webview.show();

3. Function Injection

The HTML does not use __ideLogin__ / __ideLogout__ — authentication and logout go through window.__ideExecuteCommand__ (same pattern as the tree view). The IDE must implement:

Function Purpose Required
window.__ideExecuteCommand__(command, args, callback?) Forward to workspace/executeCommand Yes
window.__saveIdeConfig__(jsonString) Persist settings from the HTML form Yes
window.__IS_IDE_AUTOSAVE_ENABLED__ If true, form changes trigger save via auto-save.js No (default false)
window.__onFormDirtyChange__(isDirty) Dirty-state callback for tab chrome No
window.__ideSaveAttemptFinished__(status) Save outcome: success, validation_error, bridge_missing, error No

Calls from ide/bridge.js:

  • Login: __ideExecuteCommand__('snyk.login', [authMethod, endpoint, insecure])
  • Logout: __ideExecuteCommand__('snyk.logout', [])
  • Save: __saveIdeConfig__(jsonString) where jsonString is JSON.stringify of the object from form-handler.collectData() (not workspace/didChangeConfiguration — the IDE maps into LSP config).

IDE-callable helpers (on window):

Function Purpose
window.getAndSaveIdeConfig() Run validation + save (same as auto-save path)
window.__isFormDirty__() Whether DirtyTracker sees unsaved edits
window.setAuthToken(token, apiUrl?) After OAuth/token login: set token / api_endpoint fields and sync dirty baseline

Injection example (conceptual):

webview.window.__ideExecuteCommand__ = (command, args, callback) => {
  client.sendRequest('workspace/executeCommand', { command, arguments: args }).then(callback);
};
webview.window.__saveIdeConfig__ = async (jsonString: string) => {
  await persistHtmlFormJsonToSnykSettings(jsonString); // IDE maps keys → LspConfigurationParam
};
webview.window.__IS_IDE_AUTOSAVE_ENABLED__ = true;
webview.window.__onFormDirtyChange__ = (isDirty: boolean) => { /* update tab title */ };

See Function Injection Flow for the detailed sequence.

4. Saving Configuration

On save (or auto-save), features/auto-save.js calls form-handler.collectData(), then JSON.stringifys the result and passes that string to window.__saveIdeConfig__(jsonString). The object uses the same pflag-oriented names as the rest of the LS (snake_case / wire names), not legacy camelCase init-option names.

Global keys (examples) — see infrastructure/configuration/template/js/ui/form-handler.js and template/config.html:

  • snyk_oss_enabled, snyk_code_enabled, snyk_iac_enabled, snyk_secrets_enabled (booleans)
  • scan_automatic, organization, api_endpoint, token, proxy_insecure, authentication_method
  • severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low (booleans)
  • issue_view_open_issues, issue_view_ignored_issues (booleans)
  • risk_score_threshold (number), scan_net_new (boolean — delta / net-new findings)
  • cli_path, automatic_download, binary_base_url, cli_release_channel
  • trusted_folders: string array (from trustedFolder_* inputs)
  • additional_parameters (string, space-separated) — Project Defaults Advanced section. Default CLI parameters for all project scans; stored as cli_additional_oss_parameters ([]string) at UserGlobalKey by applyCliConfig. These combine with per-project additional_parameters: both sets of tokens are used together. Not LDX-Sync routed.
  • additional_environment (string, semicolon-separated KEY=VALUE pairs) — Project Defaults Advanced section. Default environment variables for all project scans; applied via os.Setenv and also persisted at UserGlobalKey(additional_environment) for dialog pre-population. Combine with per-project values: per-project overrides win on key conflict. Not LDX-Sync routed.

Project Defaults Advanced section: The {{FolderLabel}} defaults tab contains a collapsible "Advanced" section (id defaults-advanced) with name="additional_parameters" and name="additional_environment" global inputs. A static info-box above the fields explains that values at both levels combine. The exact UX copy is TBD (Tars to finalize, per IDE-2110 comment 2026-06-05).

Per folder: folderConfigs is an array of objects with keys such as folderPath, preferred_org, additional_parameters (array of CLI tokens), additional_environment, org_set_by_user, scan_command_config, plus folder-scope overrides (scan_automatic, severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low, snyk_oss_enabled, …). Folder field name= attributes use the folder_<index>_<setting> convention (e.g. folder_0_severity_filter_critical); form-handler.collectData() parses the index and writes the suffix as a flat key under folderConfigs[i]. Per-folder additional_parameters arrives as a []string (split on whitespace by form-handler.js); per-folder additional_environment arrives as a raw string.

From IDE to LS (protocol v25): the plugin must translate the saved JSON into workspace/didChangeConfiguration using the nested envelope documented in configuration.md and types.DidChangeConfigurationParams — the LSP settings field wraps an LspConfigurationParam (settings map of ConfigSetting, folderConfigs, trustedFolders). Keys in the maps are pflag names (e.g. snyk_oss_enabled, api_endpoint).

Processing (server): UpdateSettings in application/server/configuration.go applies machine and folder maps, respects changed on each ConfigSetting, then persists and notifies (e.g. $/snyk.configuration).

Important notes:

  • HTML → IDE is a JSON string; parsing and mapping to LSP are IDE-side.
  • Tokens and other write-only settings follow the same resolution rules as in configuration.md.

See Saving Configuration Flow for the sequence diagram.

4a. Resetting folder overrides

The per-folder "Reset overrides" button (one per folder tab, top-right in the disclaimer banner) clears all of that folder's user overrides so the effective values fall back to org / LDX-sync / default.

JS side (snyk-ls, already implemented): clicking the button marks the folder by its folderPath (read from the hidden folder_<index>_folderPath input — not the index, which is compacted away during diffing). On save, form-handler.applyFolderResets() emits flat null for each of these 17 folder fields on that folder's entry in folderConfigs:

scan_automatic, scan_net_new,
severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low,
snyk_oss_enabled, snyk_code_enabled, snyk_iac_enabled, snyk_secrets_enabled,
issue_view_open_issues, issue_view_ignored_issues, risk_score_threshold,
preferred_org,
additional_parameters, additional_environment, scan_command_config

A reset is emitted even if the folder has no other edits — a reset-only folder still appears in the outbound folderConfigs keyed by folderPath. The JS deliberately emits flat snake_case null, not a ConfigSetting envelope; building the envelope is the IDE plugin's job (next paragraph).

The IDE plugin MUST map a flat null folder field → {value: null, changed: true} for that folder key when building workspace/didChangeConfiguration. This is the cross-IDE reset contract:

Saved JSON folder field IDE maps to ConfigSetting LS effect
absent omit from the map no-op
present, non-null value {value: X, changed: true} set user:folder:<path>:<key>
present, null {value: null, changed: true} Unset the user:folder: override → fall back

A plugin that treats a flat null as "absent" (e.g. nullable-type ?.let{}, node.isNull() → continue, .HasValue == false) will silently drop the reset and the button will appear to do nothing. The null must survive deserialization (parse the raw JSON tree to distinguish present-with-null from absent when the data model can't) and reach the LS as {value: null, changed: true}. See the reset contract in configuration.md.

Reset overrides (Project Defaults)

The {{FolderLabel}} defaults (Project Defaults) tab has a Reset overrides button (.reset-global-overrides-btn, rendered in the fallbacks-pane info-box of config.html). Clicking it calls formHandler.markGlobalForReset(), which sets ConfigApp.globalReset = true. On the next save, auto-save.js calls formHandler.applyGlobalResets(data), which writes every key in GLOBAL_RESET_FIELDS (the 14 org-scope keys, kept in sync with the Go types.GlobalResettableSettings) as null at the top level of the saved JSON — never inside folderConfigs — and clears the flag. The IDE translates each top-level key: null into a {changed: true, value: null} ConfigSetting, which the LS treats as a global reset: it clears the user:global override and reverts the value to the LDX-Sync / org / flagset default (organization reverts to the web account's preferred org). See Global Reset in configuration.md.

This is distinct from the per-section Reset buttons (.reset-section-btn, handled by reset-handler.js): a section reset only writes hard-coded constant defaults back into the form fields client-side (it does not clear user:global overrides server-side), whereas Reset overrides clears the stored overrides so the effective values fall back through the precedence chain. It also mirrors the per-folder Reset overrides button (.reset-overrides-btn), which clears user:folder overrides for a single folder rather than the machine-wide user:global layer.

Secure At Inception (VS Code only)

The dialog renders the Secure At Inception section only when configuration.INTEGRATION_NAME is exactly VS_CODE. It serializes two top-level machine-scope values: auto_configure_mcp_server as a boolean and secure_at_inception_execution_frequency as one of On Code Generation, Smart Scan, or Manual. A blank frequency is displayed as Manual without persisting a value merely by rendering the dialog.

The VS Code bridge maps these LS machine-scope values to window-scoped VS Code settings; neither value belongs in folderConfigs. The Secure At Inception section Reset action sets the form values to false and Manual. JetBrains, Eclipse, Visual Studio, and integrations with a blank name intentionally do not render the Secure At Inception section because their configuration bridges do not persist these values.

5. Authentication Flow

When the user clicks Authenticate, features/authentication.js calls ConfigApp.ideBridge.login(authMethod, endpoint, insecure), which invokes window.__ideExecuteCommand__('snyk.login', [authMethod, endpoint, insecure]).

IDE responsibilities:

  1. Run the Snyk login flow (OAuth, API token, or PAT per IDE).
  2. On success, call window.setAuthToken(token, apiUrl?) so the form fields token and optionally api_endpoint update (and dirty baseline syncs).
  3. Persist auth via your normal path (LS token + didChangeConfiguration / internal APIs as your plugin already does).

Do not rely on sending a flat { token } object through didChangeConfiguration unless you map it to ConfigSetting entries with pflag keys (token, api_endpoint, …).

See Authentication Flow for the detailed sequence.

6. Logout Flow

When the user clicks Logout, ideBridge.logout() runs window.__ideExecuteCommand__('snyk.logout', []).

IDE responsibilities: clear credentials, complete server-side logout, refresh UI / webview as needed.

See Logout Flow for the detailed sequence.

7. Dirty Tracking

The dialog includes a dirty tracking system that monitors form changes and notifies the IDE when there are unsaved changes.

How it Works:

  1. Initial State Capture: When the dialog loads, the DirtyTracker captures a deep clone of the initial form state
  2. Change Detection: Form inputs are monitored with event listeners - text inputs and textareas use input and change events, while select dropdowns and checkboxes use change events
  3. Deep Comparison: Before comparison, values are normalized (empty strings → null, "true"/"false" → booleans, numeric strings → numbers). The tracker then performs deep equality checks between normalized current and original state
  4. State Transition Events: When dirty state transitions (clean→dirty or dirty→clean), window.__onFormDirtyChange__(isDirty) is called
  5. Reset After Save: After successful save, the tracker resets with the new saved state as the baseline

IDE Integration:

// Listen for dirty state changes
webview.window.__onFormDirtyChange__ = (isDirty: boolean) => {
  if (isDirty) {
    // Show unsaved changes indicator
    setDocumentIcon("*");
    enableSaveButton();
  } else {
    // Clear indicator
    setDocumentIcon("");
    disableSaveButton();
  }
};

// Check dirty state before closing dialog
function beforeClose() {
  if (webview.window.__isFormDirty__()) {
    const shouldClose = confirm("You have unsaved changes. Close anyway?");
    if (!shouldClose) return false;
  }
  return true;
}

// After successful save, the HTML calls dirtyTracker.reset(data) internally; do not rely on a separate __resetDirtyState__
async function handleSave(jsonString: string) {
  try {
    await saveConfiguration(jsonString);
  } catch (error) {
    showError('Failed to save: ' + error.message);
  }
}

Features:

  • Deep equality comparison handles nested objects and arrays
  • Value normalization (empty strings = null, "true"/"false" to booleans)
  • Debounced change detection for performance
  • Automatic reset after successful save

Sequence Diagrams

Opening Configuration Dialog

Opening Configuration Dialog

sequenceDiagram
    participant IDE as IDE Client
    participant LSP as Language Server
    participant Config as Config System
    participant HTML as HTML Renderer
    
    IDE->>LSP: workspace/executeCommand<br/>{command: "snyk.workspace.configuration"}
    
    LSP->>Config: Get current configuration
    Config-->>LSP: Configuration data
    
    LSP->>Config: Construct Settings from Config
    Config-->>LSP: Settings object
    
    LSP->>HTML: Generate HTML with settings
    HTML-->>LSP: HTML content
    
    LSP-->>IDE: Command response<br/>"<html>..." (HTML string)
    
    IDE->>IDE: Create webview
    IDE->>IDE: Inject IDE functions into HTML
    IDE->>IDE: Display HTML in webview
Loading

Function Injection Flow

Function Injection Flow

sequenceDiagram
    participant IDE as IDE Client
    participant Webview as Webview Component
    participant HTML as HTML Content

    IDE->>IDE: Receive HTML content

    IDE->>Webview: Create webview instance

    IDE->>Webview: Expose functions on window object:<br/>- window.__ideExecuteCommand__(cmd, args)<br/>- window.__saveIdeConfig__(jsonString)<br/>- window.__onFormDirtyChange__(isDirty)

    IDE->>Webview: Load HTML content

    Webview-->>IDE: Webview ready

    Note over HTML,Webview: User interacts with dialog

    HTML->>Webview: User clicks "Authenticate"
    Webview->>IDE: Call __ideExecuteCommand__('snyk.login', [...])
    IDE->>IDE: Handle authentication

    HTML->>Webview: User clicks "Save"
    Webview->>Webview: collectData()
    Webview->>IDE: Call window.__saveIdeConfig__(jsonString)
    IDE->>IDE: Map JSON → didChangeConfiguration / persist

    HTML->>Webview: User clicks "Logout"
    Webview->>IDE: Call __ideExecuteCommand__('snyk.logout', [])
    IDE->>IDE: Handle logout
Loading

Saving Configuration Flow

Saving Configuration Flow

sequenceDiagram
    participant User as User
    participant Dialog as Configuration Dialog
    participant Webview as Webview
    participant IDE as IDE Client
    participant LSP as Language Server
    participant Config as Config System
    
    User->>Dialog: Modify settings
    User->>Dialog: Click "Save Configuration"
    
    Dialog->>Dialog: collectData()<br/>Gather all form values
    
    Dialog->>Webview: Call window.__saveIdeConfig__(jsonString)
    
    Webview->>IDE: Post message with config data
    
    IDE->>IDE: Validate configuration data
    
    IDE->>LSP: workspace/didChangeConfiguration<br/>{settings: data}
    
    LSP->>Config: Update configuration
    Config->>Config: Validate settings
    Config->>Config: Apply settings
    Config->>Config: Persist to storage
    Config-->>LSP: Configuration updated
    
    LSP-->>IDE: Acknowledgment
    
    IDE->>Webview: Show success message
    Webview->>Dialog: Display "Configuration saved"
    Dialog->>User: Visual feedback
Loading

Authentication Flow

Authentication Flow

sequenceDiagram
    participant User as User
    participant Dialog as Configuration Dialog
    participant IDE as IDE Client
    participant Auth as Auth Service
    participant LSP as Language Server
    participant Snyk as Snyk API
    
    User->>Dialog: Click "Authenticate"
    Dialog->>IDE: Call window.ideLogin()
    
    IDE->>Auth: Initiate authentication
    
    alt OAuth Flow
        Auth->>Snyk: Request OAuth authorization
        Snyk-->>User: Open browser with auth page
        User->>Snyk: Approve authorization
        Snyk->>Auth: OAuth callback with code
        Auth->>Snyk: Exchange code for token
        Snyk-->>Auth: Access token
    else Token Authentication
        Auth->>User: Prompt for API token
        User->>Auth: Provide token
    end
    
    Auth-->>IDE: Authentication successful<br/>{token: "..."}
    
    IDE->>LSP: workspace/didChangeConfiguration<br/>{settings: {token: "..."}}
    
    LSP->>Snyk: Verify token
    Snyk-->>LSP: Token valid
    
    LSP-->>IDE: Configuration updated
    
    IDE->>Dialog: Refresh with authenticated state
    Dialog->>User: Show "Authenticated" status
Loading

Logout Flow

Logout Flow

sequenceDiagram
    participant User as User
    participant Dialog as Configuration Dialog
    participant IDE as IDE Client
    participant LSP as Language Server
    participant Auth as Auth Service
    participant Storage as Credential Storage
    
    User->>Dialog: Click "Logout"
    Dialog->>IDE: __ideExecuteCommand__('snyk.logout', [])
    
    IDE->>LSP: workspace/executeCommand<br/>{command: "snyk.logout"}
    
    LSP->>Auth: Clear authentication state
    Auth->>Storage: Remove credentials
    Storage-->>Auth: Credentials cleared
    Auth-->>LSP: Logout complete
    
    LSP-->>IDE: Command completed
    
    IDE->>IDE: Clear local token storage
    
    IDE->>Dialog: Refresh with logged-out state
    Dialog->>User: Show "Not authenticated" status
Loading

Implementation Checklist

Basic Integration

  • Execute snyk.workspace.configuration command
  • Use returned string as webview HTML
  • Create webview/browser component for display
  • Expose window.__ideExecuteCommand__, window.__saveIdeConfig__, and optional __onFormDirtyChange__ / __ideSaveAttemptFinished__
  • Display HTML content in webview

Configuration Management

  • Implement window.__saveIdeConfig__(jsonString) to receive JSON.stringify output from the form
  • Parse JSON and map keys to ConfigSetting maps (pflag names); send workspace/didChangeConfiguration with the LspConfigurationParam envelope (see types.DidChangeConfigurationParams)
  • Validate configuration data before sending
  • Handle configuration errors gracefully
  • Provide user feedback on save success/failure (__ideSaveAttemptFinished__ / UI)
  • Rely on HTML dirtyTracker.reset after successful save (no separate __resetDirtyState__)

Authentication

  • Implement window.__ideExecuteCommand__ so snyk.login can run with optional [authMethod, endpoint, insecure]
  • Support OAuth flow (recommended)
  • Support PAT (Personal Access Token) authentication
  • Support API token authentication (legacy)
  • Update language server on successful authentication
  • Handle authentication errors
  • Optionally refresh dialog after authentication

Logout

  • Route logout through __ideExecuteCommand__('snyk.logout', []) (not a separate __ideLogout__ on the window)
  • Execute snyk.logout command
  • Clear stored credentials
  • Update UI to reflect logged-out state
  • Optionally refresh dialog after logout

Dirty Tracking (Optional but Recommended)

  • Implement window.__onFormDirtyChange__(isDirty) callback
  • Show unsaved changes indicator in IDE UI (e.g., "*" in tab title)
  • Warn user before closing dialog with unsaved changes using window.__isFormDirty__()
  • Disable save button when form is clean
  • Enable save button when form is dirty

Auto-Save (Optional)

  • Set window.__IS_IDE_AUTOSAVE_ENABLED__ = true before loading HTML
  • Handle automatic saves triggered by form changes
  • Provide feedback for auto-save operations

User Experience

  • Display loading indicators during operations
  • Show success/error messages
  • Provide validation feedback for form fields
  • Support dialog refresh after configuration changes
  • Handle dialog close/cancel actions
  • Implement beforeunload confirmation for unsaved changes

Best Practices

  1. Error Handling: Always wrap LSP calls in try-catch blocks and provide meaningful error messages to users.

  2. Validation: Validate configuration data on the IDE side before sending to the language server.

  3. Security:

    • Never log or expose authentication tokens
    • Use secure credential storage
    • Clear sensitive data on logout
  4. User Feedback: Provide immediate visual feedback for all user actions (save, authenticate, logout).

  5. Refresh Strategy: After authentication or logout, consider refreshing the dialog to show the updated state.

  6. Webview Isolation: Use proper webview security settings to isolate the dialog from other IDE components.

Troubleshooting

Dialog doesn't open

  • Verify the command ID is correct: snyk.workspace.configuration
  • Check that the language server is initialized
  • Ensure workspace/executeCommand capability is supported

Functions not working

  • Verify function injection is replacing all placeholders
  • Check that webview message passing is configured correctly
  • Ensure functions are exposed to the webview's global scope

Configuration not saving

  • Verify the workspace/didChangeConfiguration notification format
  • Check that the language server has workspace.configuration capability
  • Validate configuration data structure

Authentication fails

  • Ensure network connectivity to Snyk API
  • Verify OAuth redirect URLs are configured correctly
  • Check that the token is valid and not expired

Related Documentation

Support

For issues or questions about the configuration dialog integration: