Translate: Update API key management

d91c6c28cb054c0de36c26187c81389c689c2062

Cohee <18619528+Cohee1207@users.noreply.github.com>

4 files changed, +36 -64Ignore whitespace
public/scripts/extensions/translate/index.html+8 -2
@@ -28,8 +28,14 @@
28 <option value="free">Free</option>28 <option value="free">Free</option>
29 <option value="pro">Pro</option>29 <option value="pro">Pro</option>
30 </select>30 </select>
31 <div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>31 <div id="translate_key_button" class="menu_button menu_button_icon margin0 manage-api-keys">
32 <div id="translate_url_button" class="menu_button fa-solid fa-link margin0"></div>32 <i class="fa-solid fa-key"></i>
33 <span>Key</span>
34 </div>
35 <div id="translate_url_button" class="menu_button menu_button_icon margin0 manage-api-keys">
36 <i class="fa-solid fa-link"></i>
37 <span>URL</span>
38 </div>
33 </div>39 </div>
34 <label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>40 <label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>
35 <select id="translation_target_language" name="target_language"></select>41 <select id="translation_target_language" name="target_language"></select>
public/scripts/extensions/translate/index.js+12 -59
@@ -10,9 +10,9 @@ import {
10 updateMessageBlock,10 updateMessageBlock,
11} from '../../../script.js';11} from '../../../script.js';
12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
13import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';13import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
14import { updateReasoningUI } from '../../reasoning.js';14import { updateReasoningUI } from '../../reasoning.js';
15import { findSecret, secret_state, writeSecret } from '../../secrets.js';15import { secret_state } from '../../secrets.js';
16import { SlashCommand } from '../../slash-commands/SlashCommand.js';16import { SlashCommand } from '../../slash-commands/SlashCommand.js';
17import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';17import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
18import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';18import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -152,9 +152,9 @@ const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva'];
152function showKeysButton() {152function showKeysButton() {
153 const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);153 const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
154 const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);154 const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);
155 $('#translate_key_button').toggle(providerRequiresKey);155 $('#translate_key_button').toggle(providerRequiresKey).data('key', extension_settings.translate.provider);
156 $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));156 $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
157 $('#translate_url_button').toggle(providerOptionalUrl);157 $('#translate_url_button').toggle(providerOptionalUrl).data('key', extension_settings.translate.provider + '_url');
158 $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));158 $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
159 $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');159 $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');
160}160}
@@ -749,63 +749,16 @@ jQuery(async () => {
749 saveSettingsDebounced();749 saveSettingsDebounced();
750 });750 });
751 $(document).on('click', '.mes_translate', onMessageTranslateClick);751 $(document).on('click', '.mes_translate', onMessageTranslateClick);
752 $('#translate_key_button').on('click', async () => {
753 const optionText = $('#translation_provider option:selected').text();
754 const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT, '', {
755 customButtons: [{
756 text: 'Remove Key',
757 appendAtEnd: true,
758 result: POPUP_RESULT.NEGATIVE,
759 action: async () => {
760 await writeSecret(extension_settings.translate.provider, '');
761 toastr.success('API Key removed');
762 $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
763 },
764 }],
765 });
766752
767 if (!key) {753 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach((eventType) => {
768 return;754 eventSource.on(eventType, (/** @type {string} */ key) => {
769 }755 if (key === extension_settings.translate.provider) {
770756 $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
771 await writeSecret(extension_settings.translate.provider, String(key));757 }
772 toastr.success('API Key saved');758 if (key === `${extension_settings.translate.provider}_url`) {
773 $('#translate_key_button').addClass('success');759 $('#translate_url_button').toggleClass('success', !!secret_state[`${extension_settings.translate.provider}_url`]);
774 });760 }
775 $('#translate_url_button').on('click', async () => {
776 const optionText = $('#translation_provider option:selected').text();
777 const exampleURLs = {
778 'libre': 'http://127.0.0.1:5000/translate',
779 'lingva': 'https://lingva.ml/api/v1',
780 'oneringtranslator': 'http://127.0.0.1:4990/translate',
781 'deeplx': 'http://127.0.0.1:1188/translate',
782 };
783 const popupText = `<h3>${optionText} API URL</h3><i>Example: <tt>${String(exampleURLs[extension_settings.translate.provider])}</tt></i>`;
784
785 const secretKey = extension_settings.translate.provider + '_url';
786 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
787
788 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl, {
789 customButtons: [{
790 text: 'Remove URL',
791 appendAtEnd: true,
792 result: POPUP_RESULT.NEGATIVE,
793 action: async () => {
794 await writeSecret(secretKey, '');
795 toastr.success('API URL removed');
796 $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]);
797 },
798 }],
799 });761 });
800
801 if (!url) {
802 return;
803 }
804
805 await writeSecret(secretKey, String(url));
806
807 toastr.success('API URL saved');
808 $('#translate_url_button').addClass('success');
809 });762 });
810763
811 loadSettings();764 loadSettings();
public/scripts/secrets.js+12 -0
@@ -23,10 +23,16 @@ export const SECRET_KEYS = {
23 OPENAI: 'api_key_openai',23 OPENAI: 'api_key_openai',
24 NOVEL: 'api_key_novel',24 NOVEL: 'api_key_novel',
25 CLAUDE: 'api_key_claude',25 CLAUDE: 'api_key_claude',
26 DEEPL: 'deepl',
27 LIBRE: 'libre',
28 LIBRE_URL: 'libre_url',
29 LINGVA_URL: 'lingva_url',
26 OPENROUTER: 'api_key_openrouter',30 OPENROUTER: 'api_key_openrouter',
27 SCALE: 'api_key_scale',31 SCALE: 'api_key_scale',
28 AI21: 'api_key_ai21',32 AI21: 'api_key_ai21',
29 SCALE_COOKIE: 'scale_cookie',33 SCALE_COOKIE: 'scale_cookie',
34 ONERING_URL: 'oneringtranslator_url',
35 DEEPLX_URL: 'deeplx_url',
30 MAKERSUITE: 'api_key_makersuite',36 MAKERSUITE: 'api_key_makersuite',
31 VERTEXAI: 'api_key_vertexai',37 VERTEXAI: 'api_key_vertexai',
32 SERPAPI: 'api_key_serpapi',38 SERPAPI: 'api_key_serpapi',
@@ -104,6 +110,12 @@ const FRIENDLY_NAMES = {
104 [SECRET_KEYS.FALAI]: 'FAL.AI',110 [SECRET_KEYS.FALAI]: 'FAL.AI',
105 [SECRET_KEYS.AZURE_TTS]: 'Azure TTS',111 [SECRET_KEYS.AZURE_TTS]: 'Azure TTS',
106 [SECRET_KEYS.AIMLAPI]: 'AI/ML API',112 [SECRET_KEYS.AIMLAPI]: 'AI/ML API',
113 [SECRET_KEYS.DEEPL]: 'DeepL',
114 [SECRET_KEYS.LIBRE]: 'LibreTranslate',
115 [SECRET_KEYS.LIBRE_URL]: 'LibreTranslate Endpoint (e.g. http://127.0.0.1:5000/translate)',
116 [SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)',
117 [SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)',
118 [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)',
107};119};
108120
109const INPUT_MAP = {121const INPUT_MAP = {
src/endpoints/secrets.js+4 -3
@@ -162,11 +162,12 @@ export class SecretManager {
162 /**162 /**
163 * Masks a secret value with asterisks in the middle163 * Masks a secret value with asterisks in the middle
164 * @param {string} value The secret value to mask164 * @param {string} value The secret value to mask
165 * @param {string} key The secret key
165 * @returns {string} A masked version of the value for peeking166 * @returns {string} A masked version of the value for peeking
166 */167 */
167 getMaskedValue(value) {168 getMaskedValue(value, key) {
168 // No masking if exposure is allowed169 // No masking if exposure is allowed
169 if (allowKeysExposure) {170 if (allowKeysExposure || EXPORTABLE_KEYS.includes(key)) {
170 return value;171 return value;
171 }172 }
172 const threshold = 10;173 const threshold = 10;
@@ -340,7 +341,7 @@ export class SecretManager {
340 if (value && Array.isArray(value) && value.length > 0) {341 if (value && Array.isArray(value) && value.length > 0) {
341 state[key] = value.map(secret => ({342 state[key] = value.map(secret => ({
342 id: secret.id,343 id: secret.id,
343 value: this.getMaskedValue(secret.value),344 value: this.getMaskedValue(secret.value, key),
344 label: secret.label,345 label: secret.label,
345 active: secret.active,346 active: secret.active,
346 }));347 }));