Translate: Update API key management
| @@ -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> |
| @@ -10,9 +10,9 @@ import { | |||
| 10 | updateMessageBlock, | 10 | updateMessageBlock, |
| 11 | } from '../../../script.js'; | 11 | } from '../../../script.js'; |
| 12 | import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; | 12 | import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 13 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js'; | 13 | import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 14 | import { updateReasoningUI } from '../../reasoning.js'; | 14 | import { updateReasoningUI } from '../../reasoning.js'; |
| 15 | import { findSecret, secret_state, writeSecret } from '../../secrets.js'; | 15 | import { secret_state } from '../../secrets.js'; |
| 16 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 16 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 17 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 17 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 18 | import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 18 | import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -152,9 +152,9 @@ const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva']; | |||
| 152 | function showKeysButton() { | 152 | function 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 | }); | ||
| 766 | 752 | ||
| 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) => { |
| 755 | if (key === extension_settings.translate.provider) { | ||
| 756 | $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]); | ||
| 769 | } | 757 | } |
| 770 | 758 | if (key === `${extension_settings.translate.provider}_url`) { | |
| 771 | await writeSecret(extension_settings.translate.provider, String(key)); | 759 | $('#translate_url_button').toggleClass('success', !!secret_state[`${extension_settings.translate.provider}_url`]); |
| 772 | toastr.success('API Key saved'); | ||
| 773 | $('#translate_key_button').addClass('success'); | ||
| 774 | }); | ||
| 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 | }); | ||
| 800 | |||
| 801 | if (!url) { | ||
| 802 | return; | ||
| 803 | } | 760 | } |
| 804 | 761 | }); | |
| 805 | await writeSecret(secretKey, String(url)); | ||
| 806 | |||
| 807 | toastr.success('API URL saved'); | ||
| 808 | $('#translate_url_button').addClass('success'); | ||
| 809 | }); | 762 | }); |
| 810 | 763 | ||
| 811 | loadSettings(); | 764 | loadSettings(); |
| @@ -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 | }; |
| 108 | 120 | ||
| 109 | const INPUT_MAP = { | 121 | const INPUT_MAP = { |
| @@ -162,11 +162,12 @@ export class SecretManager { | |||
| 162 | /** | 162 | /** |
| 163 | * Masks a secret value with asterisks in the middle | 163 | * Masks a secret value with asterisks in the middle |
| 164 | * @param {string} value The secret value to mask | 164 | * @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 peeking | 166 | * @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 allowed | 169 | // 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 | })); |