import { DOMPurify, moment } from '../lib.js'; import { event_types, eventSource, getRequestHeaders } from '../script.js'; import { t } from './i18n.js'; import { chat_completion_sources } from './openai.js'; import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { renderTemplateAsync } from './templates.js'; import { textgen_types } from './textgen-settings.js'; import { copyText, isTrueBoolean } from './utils.js'; export const SECRET_KEYS = { HORDE: 'api_key_horde', MANCER: 'api_key_mancer', VLLM: 'api_key_vllm', APHRODITE: 'api_key_aphrodite', TABBY: 'api_key_tabby', OPENAI: 'api_key_openai', NOVEL: 'api_key_novel', CLAUDE: 'api_key_claude', OPENROUTER: 'api_key_openrouter', SCALE: 'api_key_scale', AI21: 'api_key_ai21', SCALE_COOKIE: 'scale_cookie', MAKERSUITE: 'api_key_makersuite', VERTEXAI: 'api_key_vertexai', SERPAPI: 'api_key_serpapi', MISTRALAI: 'api_key_mistralai', TOGETHERAI: 'api_key_togetherai', INFERMATICAI: 'api_key_infermaticai', DREAMGEN: 'api_key_dreamgen', CUSTOM: 'api_key_custom', OOBA: 'api_key_ooba', NOMICAI: 'api_key_nomicai', KOBOLDCPP: 'api_key_koboldcpp', LLAMACPP: 'api_key_llamacpp', COHERE: 'api_key_cohere', PERPLEXITY: 'api_key_perplexity', GROQ: 'api_key_groq', AZURE_TTS: 'api_key_azure_tts', FEATHERLESS: 'api_key_featherless', ZEROONEAI: 'api_key_01ai', HUGGINGFACE: 'api_key_huggingface', STABILITY: 'api_key_stability', CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', NANOGPT: 'api_key_nanogpt', TAVILY: 'api_key_tavily', BFL: 'api_key_bfl', GENERIC: 'api_key_generic', DEEPSEEK: 'api_key_deepseek', SERPER: 'api_key_serper', AIMLAPI: 'api_key_aimlapi', FALAI: 'api_key_falai', XAI: 'api_key_xai', VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', }; const FRIENDLY_NAMES = { [SECRET_KEYS.HORDE]: 'AI Horde', [SECRET_KEYS.MANCER]: 'Mancer', [SECRET_KEYS.OPENAI]: 'OpenAI', [SECRET_KEYS.NOVEL]: 'NovelAI', [SECRET_KEYS.CLAUDE]: 'Claude', [SECRET_KEYS.OPENROUTER]: 'OpenRouter', [SECRET_KEYS.SCALE]: 'Scale', [SECRET_KEYS.AI21]: 'AI21', [SECRET_KEYS.SCALE_COOKIE]: 'Scale (Cookie)', [SECRET_KEYS.MAKERSUITE]: 'Google AI Studio', [SECRET_KEYS.VERTEXAI]: 'Google Vertex AI (Express Mode)', [SECRET_KEYS.VLLM]: 'vLLM', [SECRET_KEYS.APHRODITE]: 'Aphrodite', [SECRET_KEYS.TABBY]: 'TabbyAPI', [SECRET_KEYS.MISTRALAI]: 'MistralAI', [SECRET_KEYS.CUSTOM]: 'Custom (OpenAI-compatible)', [SECRET_KEYS.TOGETHERAI]: 'TogetherAI', [SECRET_KEYS.OOBA]: 'Text Generation WebUI', [SECRET_KEYS.INFERMATICAI]: 'InfermaticAI', [SECRET_KEYS.DREAMGEN]: 'DreamGen', [SECRET_KEYS.NOMICAI]: 'NomicAI', [SECRET_KEYS.KOBOLDCPP]: 'KoboldCpp', [SECRET_KEYS.LLAMACPP]: 'llama.cpp', [SECRET_KEYS.COHERE]: 'Cohere', [SECRET_KEYS.PERPLEXITY]: 'Perplexity', [SECRET_KEYS.GROQ]: 'Groq', [SECRET_KEYS.FEATHERLESS]: 'Featherless', [SECRET_KEYS.ZEROONEAI]: '01.AI', [SECRET_KEYS.HUGGINGFACE]: 'HuggingFace', [SECRET_KEYS.NANOGPT]: 'NanoGPT', [SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)', [SECRET_KEYS.DEEPSEEK]: 'DeepSeek', [SECRET_KEYS.XAI]: 'xAI (Grok)', [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: 'Google Vertex AI (Service Account)', [SECRET_KEYS.STABILITY]: 'Stability AI', [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS', [SECRET_KEYS.TAVILY]: 'Tavily', [SECRET_KEYS.BFL]: 'Black Forest Labs', [SECRET_KEYS.SERPAPI]: 'SerpApi', [SECRET_KEYS.SERPER]: 'Serper', [SECRET_KEYS.FALAI]: 'FAL.AI', [SECRET_KEYS.AZURE_TTS]: 'Azure TTS', [SECRET_KEYS.AIMLAPI]: 'AI/ML API', }; const INPUT_MAP = { [SECRET_KEYS.HORDE]: '#horde_api_key', [SECRET_KEYS.MANCER]: '#api_key_mancer', [SECRET_KEYS.OPENAI]: '#api_key_openai', [SECRET_KEYS.NOVEL]: '#api_key_novel', [SECRET_KEYS.CLAUDE]: '#api_key_claude', [SECRET_KEYS.OPENROUTER]: '.api_key_openrouter', [SECRET_KEYS.SCALE]: '#api_key_scale', [SECRET_KEYS.AI21]: '#api_key_ai21', [SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie', [SECRET_KEYS.MAKERSUITE]: '#api_key_makersuite', [SECRET_KEYS.VERTEXAI]: '#api_key_vertexai', [SECRET_KEYS.VLLM]: '#api_key_vllm', [SECRET_KEYS.APHRODITE]: '#api_key_aphrodite', [SECRET_KEYS.TABBY]: '#api_key_tabby', [SECRET_KEYS.MISTRALAI]: '#api_key_mistralai', [SECRET_KEYS.CUSTOM]: '#api_key_custom', [SECRET_KEYS.TOGETHERAI]: '#api_key_togetherai', [SECRET_KEYS.OOBA]: '#api_key_ooba', [SECRET_KEYS.INFERMATICAI]: '#api_key_infermaticai', [SECRET_KEYS.DREAMGEN]: '#api_key_dreamgen', [SECRET_KEYS.NOMICAI]: '#api_key_nomicai', [SECRET_KEYS.KOBOLDCPP]: '#api_key_koboldcpp', [SECRET_KEYS.LLAMACPP]: '#api_key_llamacpp', [SECRET_KEYS.COHERE]: '#api_key_cohere', [SECRET_KEYS.PERPLEXITY]: '#api_key_perplexity', [SECRET_KEYS.GROQ]: '#api_key_groq', [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless', [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai', [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface', [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt', [SECRET_KEYS.GENERIC]: '#api_key_generic', [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek', [SECRET_KEYS.AIMLAPI]: '#api_key_aimlapi', [SECRET_KEYS.XAI]: '#api_key_xai', [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json', }; const getLabel = () => moment().format('L LT'); /** * Resolves the secret key based on the selected API, chat completion source, and text completion type. * @returns {string|null} The secret key corresponding to the selected API, or null if no key is found. */ export function resolveSecretKey() { const { mainApi, chatCompletionSettings, textCompletionSettings } = SillyTavern.getContext(); const chatCompletionSource = chatCompletionSettings.chat_completion_source; const textCompletionType = textCompletionSettings.type; if (mainApi === 'koboldhorde') { return SECRET_KEYS.HORDE; } if (mainApi === 'novel') { return SECRET_KEYS.NOVEL; } if (mainApi === 'textgenerationwebui') { const [key] = Object.entries(textgen_types).find(([, value]) => value === textCompletionType) ?? [null]; if (key && SECRET_KEYS[key]) { return SECRET_KEYS[key]; } } if (mainApi === 'openai') { if (chatCompletionSource === chat_completion_sources.VERTEXAI) { switch (chatCompletionSettings.vertexai_auth_mode) { case 'express': return SECRET_KEYS.VERTEXAI; case 'full': return SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT; } } if (chatCompletionSource === chat_completion_sources.SCALE) { return chatCompletionSettings.use_alt_scale ? SECRET_KEYS.SCALE_COOKIE : SECRET_KEYS.SCALE; } const [key] = Object.entries(chat_completion_sources).find(([, value]) => value === chatCompletionSource) ?? [null]; if (key && SECRET_KEYS[key]) { return SECRET_KEYS[key]; } } return null; } /** * Gets the label of a secret by its ID. * @param {string} id The ID of the secret to find. * @returns {string} The label of the secret with the given ID, or an empty string if not found. */ export function getSecretLabelById(id) { for (const key of Object.values(SECRET_KEYS)) { const secrets = secret_state[key]; if (!Array.isArray(secrets)) { continue; } const secret = secrets.find(s => s.id === id); if (secret) { return `${secret.label} (${secret.value})`; } } return ''; } export function updateSecretDisplay() { for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) { const validSecret = !!secret_state[secret_key]; const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text'); const label = getActiveSecretLabel(secret_key); const placeholderWithLabel = label ? `${placeholder} (${label})` : placeholder; $(input_selector).attr('placeholder', placeholderWithLabel); } } /** * Gets the active secret label for a given key. * @param {string} key Gets the active secret label for a given key. * @returns {string} The label of the active secret, or '[No label]' if none is active. */ function getActiveSecretLabel(key) { const selectedSecret = secret_state[key]; if (Array.isArray(selectedSecret)) { const activeSecret = selectedSecret.find(x => x.active); if (!activeSecret) { return ''; } return activeSecret.label || activeSecret.value || t`[No label]`; } return ''; } async function viewSecrets() { const response = await fetch('/api/secrets/view', { method: 'POST', headers: getRequestHeaders(), }); if (response.status == 403) { await Popup.show.text(t`Forbidden`, t`To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.`); return; } if (!response.ok) { return; } const data = await response.json(); const table = document.createElement('table'); table.classList.add('responsiveTable'); $(table).append('KeyValue'); for (const [key, value] of Object.entries(data)) { $(table).append(`${DOMPurify.sanitize(key)}${DOMPurify.sanitize(value)}`); } await callGenericPopup(table.outerHTML, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true }); } /** * @type {import('../../src/endpoints/secrets.js').SecretStateMap} */ export let secret_state = {}; /** * Write a secret value to the server. * @param {string} key Secret key * @param {string} value Secret value to write * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically. * @return {Promise} The ID of the newly created secret key, or null if no value is provided. */ export async function writeSecret(key, value, label) { try { if (!value) { console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); await deleteSecret(key); return null; } if (!label) { label = getLabel(); } const response = await fetch('/api/secrets/write', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ key, value, label }), }); if (!response.ok) { return null; } const { id } = await response.json(); // Clear the input field $(INPUT_MAP[key]).val('').trigger('input'); await readSecretState(); await eventSource.emit(event_types.SECRET_WRITTEN, key); return id; } catch (error) { console.error(`Could not write secret value: ${key}`, error); return null; } } /** * Deletes a secret value from the server. * @param {string} key Secret key * @param {string} [id] (Optional) ID of the secret key to delete. If not provided, deletes an active key. */ export async function deleteSecret(key, id) { try { const response = await fetch('/api/secrets/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ key, id }), }); if (response.ok) { await readSecretState(); // Force reconnection to the API with the new key $('#main_api').trigger('change'); await eventSource.emit(event_types.SECRET_DELETED, key); } } catch (error) { console.error(`Could not delete secret value: ${key}`, error); } } /** * Reads the current state of secrets from the server. * @returns {Promise} */ export async function readSecretState() { try { const response = await fetch('/api/secrets/read', { method: 'POST', headers: getRequestHeaders(), }); if (response.ok) { secret_state = await response.json(); updateSecretDisplay(); updateInputDataLists(); await checkOpenRouterAuth(); } } catch { console.error('Could not read secrets file'); } } /** * Finds a secret value by key. * @param {string} key Secret key * @param {string} [id] ID of the secret to find. If not provided, will return the active secret. * @returns {Promise} Secret value, or null if keys are not exposed */ export async function findSecret(key, id) { try { const response = await fetch('/api/secrets/find', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ key, id }), }); if (!response.ok) { return null; } const data = await response.json(); return data.value; } catch { console.error('Could not find secret value: ', key); return null; } } /** * Changes the active value for a given secret key. * @param {string} key Secret key to rotate * @param {string} id ID of the secret to rotate */ export async function rotateSecret(key, id) { try { const response = await fetch('/api/secrets/rotate', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ key, id }), }); if (response.ok) { await readSecretState(); // Force reconnection to the API with the new key $('#main_api').trigger('change'); await eventSource.emit(event_types.SECRET_ROTATED, key); } } catch (error) { console.error(`Could not rotate secret value: ${key}`, error); } } /** * Renames a secret value on the server. * @param {string} key Secret key to rename * @param {string} id ID of the secret to rename * @param {string} label Label to rename the secret to */ export async function renameSecret(key, id, label) { try { const response = await fetch('/api/secrets/rename', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ key, id, label }), }); if (response.ok) { await readSecretState(); await eventSource.emit(event_types.SECRET_EDITED, key); } } catch (error) { console.error(`Could not rename secret value: ${key}`, error); } } /** * Redirects the user to authorize OpenRouter. */ function authorizeOpenRouter() { const redirectUrl = new URL('/callback/openrouter', window.location.origin); const openRouterUrl = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(redirectUrl.toString())}`; location.href = openRouterUrl; } /** * Checks if the OpenRouter authorization code is present in the URL, and if so, exchanges it for an API key. * @returns {Promise} */ async function checkOpenRouterAuth() { const params = new URLSearchParams(location.search); const source = params.get('source'); if (source === 'openrouter') { const query = new URLSearchParams(params.get('query')); const code = query.get('code'); try { const response = await fetch('https://openrouter.ai/api/v1/auth/keys', { method: 'POST', body: JSON.stringify({ code }), }); if (!response.ok) { throw new Error('OpenRouter exchange error'); } const data = await response.json(); if (!data || !data.key) { throw new Error('OpenRouter invalid response'); } await writeSecret(SECRET_KEYS.OPENROUTER, data.key); if (secret_state[SECRET_KEYS.OPENROUTER]) { toastr.success('OpenRouter token saved'); // Remove the code from the URL const currentUrl = window.location.href; const urlWithoutSearchParams = currentUrl.split('?')[0]; window.history.pushState({}, '', urlWithoutSearchParams); } else { throw new Error('OpenRouter token not saved'); } } catch (err) { toastr.error('Could not verify OpenRouter token. Please try again.'); return; } } } /** * Updates the input data lists for secret keys for autocomplete functionality. */ function updateInputDataLists() { let container = document.getElementById('secrets_datalists'); if (!container) { container = document.createElement('div'); container.id = 'secrets_datalists'; container.style.display = 'none'; document.body.appendChild(container); } for (const [key, inputSelector] of Object.entries(INPUT_MAP)) { const inputElements = document.querySelectorAll(inputSelector); if (inputElements.length === 0) { console.warn(`No input elements found for key: ${key}`); continue; } const dataListId = `${key}_datalist`; let dataList = document.getElementById(dataListId); if (!dataList) { dataList = document.createElement('datalist'); dataList.id = dataListId; container.appendChild(dataList); } // Clear existing options dataList.innerHTML = ''; const secrets = secret_state[key]; if (!Array.isArray(secrets)) { continue; } for (const secret of secrets) { const option = document.createElement('option'); option.value = secret.id; option.textContent = `${secret.label} (${secret.value})`; dataList.appendChild(option); } // Set the input element to use the datalist inputElements.forEach(element => { element.setAttribute('list', dataListId); }); } } /** * Opens the key manager dialog for a specific key. * @param {string} key Key for which to open the key manager dialog. */ async function openKeyManagerDialog(key) { const name = FRIENDLY_NAMES[key] || key; const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); template.find('button[data-action="add-secret"]').on('click', async function () { let label = ''; const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value:`, '', { customInputs: [{ id: 'newSecretLabel', type: 'text', label: t`Enter a label for the secret (optional):`, }], onClose: popup => { if (popup.result) { label = popup.inputResults.get('newSecretLabel').toString().trim(); } }, }); if (!value) { return; } await writeSecret(key, value, label); await renderSecretsList(); }); await renderSecretsList(); await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, onOpen: scrollToActive }); async function renderSecretsList() { const secrets = secret_state[key] ?? []; const list = template.find('.secretKeyManagerList'); const previousScrollTop = list.scrollTop(); const emptyMessage = template.find('.secretKeyManagerListEmpty'); emptyMessage.toggle(secrets.length === 0); const itemBlocks = []; for (const secret of secrets) { const itemTemplate = $(await renderTemplateAsync('secretKeyManagerListItem', secret)); itemTemplate.find('[data-action="copy-id"]').on('click', async function () { await copyText(secret.id); toastr.info(t`Secret ID copied to clipboard.`); }); itemTemplate.find('button[data-action="rotate-secret"]').on('click', async function () { await rotateSecret(key, secret.id); await renderSecretsList(); }); itemTemplate.find('button[data-action="copy-secret"]').on('click', async function () { const secretValue = await findSecret(key, secret.id); if (secretValue === null) { toastr.error(t`The key exposure might be disabled by the server config.`, t`Failed to copy secret value`); return; } await copyText(secretValue); toastr.info(t`Secret value copied to clipboard.`); }); itemTemplate.find('button[data-action="rename-secret"]').on('click', async function () { const label = await Popup.show.input(t`Rename Secret`, t`Enter new label for the secret:`, secret?.label || getLabel()); if (!label) { return; } await renameSecret(key, secret.id, label); await renderSecretsList(); }); itemTemplate.find('button[data-action="delete-secret"]').on('click', async function () { const confirm = await Popup.show.confirm(t`Delete Secret: ${secret?.label}`, t`Are you sure you want to delete this secret? This action cannot be undone.`); if (!confirm) { return; } await deleteSecret(key, secret.id); await renderSecretsList(); }); itemBlocks.push(itemTemplate); } list.empty().append(itemBlocks).scrollTop(previousScrollTop); } function scrollToActive() { const list = template.find('.secretKeyManagerList'); const activeKey = list.find('.active'); if (activeKey.length > 0) { const activeKeyScrollTop = activeKey.position().top + list.scrollTop() - list.height() / 2; list.scrollTop(activeKeyScrollTop); } } } function registerSecretSlashCommands() { const secretKeyEnumProvider = () => Object.values(SECRET_KEYS).map(key => new SlashCommandEnumValue(key, FRIENDLY_NAMES[key] || key, enumTypes.name, enumIcons.key)); const secretIdEnumProvider = (/** @type {SlashCommandExecutor} */ executor, /** @type {SlashCommandScope} */ _scope) => { const key = executor?.namedArgumentList?.find(x => x.name === 'key')?.value?.toString() || resolveSecretKey(); if (!key || !secret_state[key] || !Array.isArray(secret_state[key]) || secret_state[key].length === 0) { return []; } return secret_state[key].map(secret => { return new SlashCommandEnumValue(secret.id, `${secret.label} (${secret.value})`, enumTypes.name, enumIcons.key); }); }; SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'secret-id', aliases: ['secret-rotate'], helpString: t`Sets the ID of a currently active secret key. Gets the ID of the secret key if no value is provided.`, returns: t`The ID of the secret key that is now active.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'quiet', description: t`Suppress toast message notifications.`, isRequired: false, defaultValue: String(false), typeList: [ARGUMENT_TYPE.BOOLEAN], }), SlashCommandNamedArgument.fromProps({ name: 'key', description: t`The key to get the secret ID for. If not provided, will use the currently active API secrets.`, isRequired: false, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretKeyEnumProvider, }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: t`The ID or a label of the secret key to set as active. If not provided, will return the currently active secret ID.`, isRequired: true, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretIdEnumProvider, }), ], callback: async (args, value) => { const quiet = isTrueBoolean(args?.quiet?.toString()); const id = value?.toString()?.trim(); const key = args?.key?.toString()?.trim() || resolveSecretKey(); if (!key) { if (!quiet) { toastr.error(t`No secret key provided, and the key can't be resolved for the currently selected API type.`); } return ''; } const secrets = secret_state[key]; if (!Array.isArray(secrets) || secrets.length === 0) { if (!quiet) { toastr.error(t`No saved secrets found for the key: ${key}`); } return ''; } if (!id) { const activeSecret = secrets.find(s => s.active); if (!activeSecret) { if (!quiet) { toastr.error(t`No active secret found for the key: ${key}`); } return ''; } return activeSecret.id; } const savedSecret = secrets.find(s => s.id === id) ?? secrets.find(s => s.label === id); if (!savedSecret) { if (!quiet) { toastr.error(t`No secret found with ID: ${id} for the key: ${key}`); } return ''; } // Set the secret as active await rotateSecret(key, savedSecret.id); if (!quiet) { toastr.success(t`Secret with ID: ${id} is now active for the key: ${key}`); } return savedSecret.id; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'secret-delete', helpString: t`Deletes a secret key by ID.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'quiet', description: t`Suppress toast message notifications.`, isRequired: false, defaultValue: String(false), typeList: [ARGUMENT_TYPE.BOOLEAN], }), SlashCommandNamedArgument.fromProps({ name: 'key', description: t`The key to delete the secret from. If not provided, will use the currently active API secrets.`, isRequired: false, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretKeyEnumProvider, }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: t`The ID or a label of the secret key to delete. If not provided, will delete the active secret.`, isRequired: true, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretIdEnumProvider, }), ], callback: async (args, value) => { const quiet = isTrueBoolean(args?.quiet?.toString()); const id = value?.toString()?.trim(); const key = args?.key?.toString()?.trim() || resolveSecretKey(); if (!key) { if (!quiet) { toastr.error(t`No secret key provided, and the key can't be resolved for the currently selected API type.`); } return ''; } const secrets = secret_state[key]; if (!Array.isArray(secrets) || secrets.length === 0) { if (!quiet) { toastr.error(t`No saved secrets found for the key: ${key}`); } return ''; } const savedSecret = secrets.find(s => s.id === id) ?? secrets.find(s => s.label === id) ?? secrets.find(s => s.active); if (!savedSecret) { if (!quiet) { toastr.error(t`No secret found with ID: ${id} for the key: ${key}`); } return ''; } // Delete the secret await deleteSecret(key, savedSecret.id); if (!quiet) { toastr.success(t`Secret with ID: ${id} has been deleted for the key: ${key}`); } return savedSecret.id; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'secret-write', helpString: t`Writes a secret key with a value and an optional label.`, returns: t`The ID of the newly created secret key.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'quiet', description: t`Suppress toast message notifications.`, isRequired: false, defaultValue: String(false), typeList: [ARGUMENT_TYPE.BOOLEAN], }), SlashCommandNamedArgument.fromProps({ name: 'key', description: t`The key to write the secret to. If not provided, will use the currently active API secrets.`, isRequired: false, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretKeyEnumProvider, }), SlashCommandNamedArgument.fromProps({ name: 'label', description: t`The label for the secret key. If not provided, will use the current date and time.`, isRequired: false, typeList: [ARGUMENT_TYPE.STRING], }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: t`The value of the secret key to write.`, isRequired: true, typeList: [ARGUMENT_TYPE.STRING], }), ], callback: async (args, value) => { const quiet = isTrueBoolean(args?.quiet?.toString()); const key = args?.key?.toString()?.trim() || resolveSecretKey(); if (!key) { if (!quiet) { toastr.error(t`No secret key provided, and the key can't be resolved for the currently selected API type.`); } return ''; } const secrets = secret_state[key]; if (!Array.isArray(secrets) || secrets.length === 0) { if (!quiet) { toastr.error(t`No saved secrets found for the key: ${key}`); } return ''; } const valueStr = value?.toString()?.trim(); if (!valueStr) { if (!quiet) { toastr.error(t`No value provided for the secret key: ${key}`); } return ''; } const label = args?.label?.toString()?.trim() || getLabel(); const id = await writeSecret(key, valueStr, label); if (!quiet) { toastr.success(t`Secret has been written for the key: ${key}`); } return id || ''; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'secret-rename', helpString: t`Renames a secret key by ID.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'quiet', description: t`Suppress toast message notifications.`, isRequired: false, defaultValue: String(false), typeList: [ARGUMENT_TYPE.BOOLEAN], }), SlashCommandNamedArgument.fromProps({ name: 'key', description: t`The key to rename the secret in. If not provided, will use the currently active API secrets.`, isRequired: false, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretKeyEnumProvider, }), SlashCommandNamedArgument.fromProps({ name: 'id', description: t`The ID of the secret to rename. If not provided, will rename the active secret.`, isRequired: true, typeList: [ARGUMENT_TYPE.STRING], }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: t`The new label for the secret key.`, isRequired: true, typeList: [ARGUMENT_TYPE.STRING], }), ], callback: async (args, value) => { const quiet = isTrueBoolean(args?.quiet?.toString()); const key = args?.key?.toString()?.trim() || resolveSecretKey(); const id = args?.id?.toString()?.trim(); if (!key) { if (!quiet) { toastr.error(t`No secret key provided, and the key can't be resolved for the currently selected API type.`); } return ''; } const secrets = secret_state[key]; if (!Array.isArray(secrets) || secrets.length === 0) { if (!quiet) { toastr.error(t`No saved secrets found for the key: ${key}`); } return ''; } const newLabel = value?.toString()?.trim(); if (!newLabel) { if (!quiet) { toastr.error(t`No new label provided for the secret key: ${key}`); } return ''; } const savedSecret = secrets.find(s => s.id === id) ?? secrets.find(s => s.label === id) ?? secrets.find(s => s.active); if (!savedSecret) { if (!quiet) { toastr.error(t`No secret found with ID: ${id} for the key: ${key}`); } return ''; } // Rename the secret await renameSecret(key, savedSecret.id, newLabel); if (!quiet) { toastr.success(t`Secret with ID: ${id} has been renamed to "${newLabel}" for the key: ${key}`); } return savedSecret.id; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'secret-read', aliases: ['secret-find', 'secret-get'], helpString: t`Reads a secret key by ID. If key exposure is disabled, this command will not work!`, returns: t`The value of the secret key.`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'quiet', description: t`Suppress toast message notifications.`, isRequired: false, defaultValue: String(false), typeList: [ARGUMENT_TYPE.BOOLEAN], }), SlashCommandNamedArgument.fromProps({ name: 'key', description: t`The key to read the secret from. If not provided, will use the currently active API secrets.`, isRequired: false, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretKeyEnumProvider, }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: t`The ID or a label of the secret key to read. If not provided, will return the currently active secret value.`, isRequired: true, typeList: [ARGUMENT_TYPE.STRING], enumProvider: secretIdEnumProvider, }), ], callback: async (args, value) => { const quiet = isTrueBoolean(args?.quiet?.toString()); const key = args?.key?.toString()?.trim() || resolveSecretKey(); const id = value?.toString()?.trim(); if (!key) { if (!quiet) { toastr.error(t`No secret key provided, and the key can't be resolved for the currently selected API type.`); } return ''; } const secrets = secret_state[key]; if (!Array.isArray(secrets) || secrets.length === 0) { if (!quiet) { toastr.error(t`No saved secrets found for the key: ${key}`); } return ''; } const savedSecret = secrets.find(s => s.id === id) ?? secrets.find(s => s.label === id) ?? secrets.find(s => s.active); if (!savedSecret) { if (!quiet) { toastr.error(t`No secret found with ID: ${id} for the key: ${key}`); } return ''; } const secretValue = await findSecret(key, savedSecret.id); if (secretValue === null) { if (!quiet) { toastr.error(t`Could not retrieve the secret value for key: ${key}. Key exposure might be disabled.`); } return ''; } return secretValue; }, })); } export async function initSecrets() { $('#viewSecrets').on('click', viewSecrets); $(document).on('click', '.manage-api-keys', async function () { const key = $(this).data('key'); if (!key || !Object.values(SECRET_KEYS).includes(key)) { console.error('Invalid key for manage-api-keys:', key); return; } await openKeyManagerDialog(key); }); $(document).on('input', Object.values(INPUT_MAP).join(','), function () { const id = $(this).attr('id'); const value = $(this).val(); // Find the key based on the entered value for (const [key, inputSelector] of Object.entries(INPUT_MAP)) { if (!value || !this.matches(inputSelector)) { continue; } const secrets = secret_state[key]; if (!Array.isArray(secrets)) { continue; } const secretMatch = secrets.find(secret => secret.id === value); if (secretMatch) { $(this).val(''); return rotateSecret(key, secretMatch.id); } } const warningElement = $(`[data-for="${id}"]`); warningElement.toggle(value.length > 0); }); $('.openrouter_authorize').on('click', authorizeOpenRouter); registerSecretSlashCommands(); }