utils.ts
  1  import {
  2    DANGEROUS_SHELL_SETTINGS,
  3    SAFE_ENV_VARS,
  4  } from '../../utils/managedEnvConstants.js'
  5  import type { SettingsJson } from '../../utils/settings/types.js'
  6  import { jsonStringify } from '../../utils/slowOperations.js'
  7  
  8  type DangerousShellSetting = (typeof DANGEROUS_SHELL_SETTINGS)[number]
  9  
 10  export type DangerousSettings = {
 11    shellSettings: Partial<Record<DangerousShellSetting, string>>
 12    envVars: Record<string, string>
 13    hasHooks: boolean
 14    hooks?: unknown
 15  }
 16  
 17  /**
 18   * Extract dangerous settings from a settings object.
 19   *
 20   * Dangerous env vars are determined by checking against SAFE_ENV_VARS -
 21   * any env var NOT in SAFE_ENV_VARS is considered dangerous.
 22   * See managedEnv.ts for the authoritative list and threat categories.
 23   */
 24  export function extractDangerousSettings(
 25    settings: SettingsJson | null | undefined,
 26  ): DangerousSettings {
 27    if (!settings) {
 28      return {
 29        shellSettings: {},
 30        envVars: {},
 31        hasHooks: false,
 32      }
 33    }
 34  
 35    // Extract dangerous shell settings
 36    const shellSettings: Partial<Record<DangerousShellSetting, string>> = {}
 37    for (const key of DANGEROUS_SHELL_SETTINGS) {
 38      const value = settings[key]
 39      if (typeof value === 'string' && value.length > 0) {
 40        shellSettings[key] = value
 41      }
 42    }
 43  
 44    // Extract dangerous env vars - any var NOT in SAFE_ENV_VARS is dangerous
 45    const envVars: Record<string, string> = {}
 46    if (settings.env && typeof settings.env === 'object') {
 47      for (const [key, value] of Object.entries(settings.env)) {
 48        if (typeof value === 'string' && value.length > 0) {
 49          // Check if this env var is NOT in the safe list
 50          if (!SAFE_ENV_VARS.has(key.toUpperCase())) {
 51            envVars[key] = value
 52          }
 53        }
 54      }
 55    }
 56  
 57    // Check for hooks
 58    const hasHooks =
 59      settings.hooks !== undefined &&
 60      settings.hooks !== null &&
 61      typeof settings.hooks === 'object' &&
 62      Object.keys(settings.hooks).length > 0
 63  
 64    return {
 65      shellSettings,
 66      envVars,
 67      hasHooks,
 68      hooks: hasHooks ? settings.hooks : undefined,
 69    }
 70  }
 71  
 72  /**
 73   * Check if settings contain any dangerous settings
 74   */
 75  export function hasDangerousSettings(dangerous: DangerousSettings): boolean {
 76    return (
 77      Object.keys(dangerous.shellSettings).length > 0 ||
 78      Object.keys(dangerous.envVars).length > 0 ||
 79      dangerous.hasHooks
 80    )
 81  }
 82  
 83  /**
 84   * Compare two sets of dangerous settings to see if the new settings
 85   * have changed or added dangerous settings compared to the old settings
 86   */
 87  export function hasDangerousSettingsChanged(
 88    oldSettings: SettingsJson | null | undefined,
 89    newSettings: SettingsJson | null | undefined,
 90  ): boolean {
 91    const oldDangerous = extractDangerousSettings(oldSettings)
 92    const newDangerous = extractDangerousSettings(newSettings)
 93  
 94    // If new settings don't have any dangerous settings, no prompt needed
 95    if (!hasDangerousSettings(newDangerous)) {
 96      return false
 97    }
 98  
 99    // If old settings didn't have dangerous settings but new does, prompt needed
100    if (!hasDangerousSettings(oldDangerous)) {
101      return true
102    }
103  
104    // Compare the dangerous settings - any change triggers a prompt
105    const oldJson = jsonStringify({
106      shellSettings: oldDangerous.shellSettings,
107      envVars: oldDangerous.envVars,
108      hooks: oldDangerous.hooks,
109    })
110    const newJson = jsonStringify({
111      shellSettings: newDangerous.shellSettings,
112      envVars: newDangerous.envVars,
113      hooks: newDangerous.hooks,
114    })
115  
116    return oldJson !== newJson
117  }
118  
119  /**
120   * Format dangerous settings as a human-readable list for the UI
121   * Only returns setting names, not values
122   */
123  export function formatDangerousSettingsList(
124    dangerous: DangerousSettings,
125  ): string[] {
126    const items: string[] = []
127  
128    // Shell settings (names only)
129    for (const key of Object.keys(dangerous.shellSettings)) {
130      items.push(key)
131    }
132  
133    // Env vars (names only)
134    for (const key of Object.keys(dangerous.envVars)) {
135      items.push(key)
136    }
137  
138    // Hooks
139    if (dangerous.hasHooks) {
140      items.push('hooks')
141    }
142  
143    return items
144  }