( pluginId: string, values: PluginOptionValues, schema: PluginOptionSchema, )
| 88 | * Clears the load cache on success so the next `loadPluginOptions` sees fresh. |
| 89 | */ |
| 90 | export function savePluginOptions( |
| 91 | pluginId: string, |
| 92 | values: PluginOptionValues, |
| 93 | schema: PluginOptionSchema, |
| 94 | ): void { |
| 95 | const nonSensitive: PluginOptionValues = {} |
| 96 | const sensitive: Record<string, string> = {} |
| 97 | |
| 98 | for (const [key, value] of Object.entries(values)) { |
| 99 | if (schema[key]?.sensitive === true) { |
| 100 | sensitive[key] = String(value) |
| 101 | } else { |
| 102 | nonSensitive[key] = value |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | // Scrub sets — see saveMcpServerUserConfig (mcpbHandler.ts) for the |
| 107 | // rationale. Only keys in THIS save are scrubbed from the other store, |
| 108 | // so partial reconfigures don't lose data. |
| 109 | const sensitiveKeysInThisSave = new Set(Object.keys(sensitive)) |
| 110 | const nonSensitiveKeysInThisSave = new Set(Object.keys(nonSensitive)) |
| 111 | |
| 112 | // secureStorage FIRST — if keychain fails, throw before touching |
| 113 | // settings.json so old plaintext (if any) stays as fallback. |
| 114 | const storage = getSecureStorage() |
| 115 | const existingInSecureStorage = |
| 116 | storage.read()?.pluginSecrets?.[pluginId] ?? undefined |
| 117 | const secureScrubbed = existingInSecureStorage |
| 118 | ? Object.fromEntries( |
| 119 | Object.entries(existingInSecureStorage).filter( |
| 120 | ([k]) => !nonSensitiveKeysInThisSave.has(k), |
| 121 | ), |
| 122 | ) |
| 123 | : undefined |
| 124 | const needSecureScrub = |
| 125 | secureScrubbed && |
| 126 | existingInSecureStorage && |
| 127 | Object.keys(secureScrubbed).length !== |
| 128 | Object.keys(existingInSecureStorage).length |
| 129 | if (Object.keys(sensitive).length > 0 || needSecureScrub) { |
| 130 | const existing = storage.read() ?? {} |
| 131 | if (!existing.pluginSecrets) { |
| 132 | existing.pluginSecrets = {} |
| 133 | } |
| 134 | existing.pluginSecrets[pluginId] = { |
| 135 | ...secureScrubbed, |
| 136 | ...sensitive, |
| 137 | } |
| 138 | const result = storage.update(existing) |
| 139 | if (!result.success) { |
| 140 | const err = new Error( |
| 141 | `Failed to save sensitive plugin options for ${pluginId} to secure storage`, |
| 142 | ) |
| 143 | logError(err) |
| 144 | throw err |
| 145 | } |
| 146 | if (result.warning) { |
| 147 | logForDebugging(`Plugin secrets save warning: ${result.warning}`, { |
no test coverage detected