Allow saving empty secrets via secret manager and slash command (#4580) * Allow saving empty secrets via secret manager and slash command Closes #4577 * Fix logic on cancel * Fix /secrets-read with empty secret value

1d279e500e99ffd6fab955a780433c474f3a1ec8

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

Signed
2 files changed, +29 -10Ignore whitespace
public/scripts/secrets.js+26 -8
@@ -2,7 +2,7 @@ import { DOMPurify, moment } from '../lib.js';
22import { event_types, eventSource, getRequestHeaders } from '../script.js';
33import { t } from './i18n.js';
44import { chat_completion_sources } from './openai.js';
55import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
66import { SlashCommand } from './slash-commands/SlashCommand.js';
77import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
88import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -293,11 +293,13 @@ export let secret_state = {};
293293 * @param {string} key Secret key
294294 * @param {string} value Secret value to write
295295 * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically.
296+ * @param {Object} [options] Additional options
297+ * @param {boolean} [options.allowEmpty] Whether to allow writing empty values. If false and value is empty, the secret will be deleted.
296298 * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided.
297299 */
298300export async function writeSecret(key, value, label, { allowEmpty } = {}) {
299301 try {
300302 if (!value && !allowEmpty) {
301303 console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`);
302304 await deleteSecret(key);
303305 return null;
@@ -558,7 +560,8 @@ async function openKeyManagerDialog(key) {
558560 const template = $(await renderTemplateAsync('secretKeyManager', { name, key }));
559561 template.find('button[data-action="add-secret"]').on('click', async function () {
560562 let label = '';
561- const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value:`, '', {
563+ let result = POPUP_RESULT.CANCELLED;
564+ const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value (can be empty):`, '', {
562565 customInputs: [{
563566 id: 'newSecretLabel',
564567 type: 'text',
@@ -567,13 +570,20 @@ async function openKeyManagerDialog(key) {
567570 onClose: popup => {
568571 if (popup.result) {
569572 label = popup.inputResults.get('newSecretLabel').toString().trim();
573+ result = popup.result;
570574 }
571575 },
572576 });
573577 if (!value) {
574- return;
578+ if (result !== POPUP_RESULT.AFFIRMATIVE) {
579+ return;
580+ }
581+ const allowEmpty = await Popup.show.confirm(t`No value entered`, t`No value was entered for the secret. Do you want to add an empty secret?`);
582+ if (!allowEmpty) {
583+ return;
584+ }
575585 }
576586 await writeSecret(key, value, label, { allowEmpty: true });
577587 await renderSecretsList();
578588 });
579589
@@ -821,6 +831,13 @@ function registerSecretSlashCommands() {
821831 isRequired: false,
822832 typeList: [ARGUMENT_TYPE.STRING],
823833 }),
834+ SlashCommandNamedArgument.fromProps({
835+ name: 'empty',
836+ description: t`Whether to allow empty values.`,
837+ isRequired: false,
838+ typeList: [ARGUMENT_TYPE.BOOLEAN],
839+ defaultValue: String(false),
840+ }),
824841 ],
825842 unnamedArgumentList: [
826843 SlashCommandArgument.fromProps({
@@ -831,6 +848,7 @@ function registerSecretSlashCommands() {
831848 ],
832849 callback: async (args, value) => {
833850 const quiet = isTrueBoolean(args?.quiet?.toString());
851+ const allowEmpty = isTrueBoolean(args?.empty?.toString());
834852 const key = args?.key?.toString()?.trim() || resolveSecretKey();
835853
836854 if (!key) {
@@ -849,7 +867,7 @@ function registerSecretSlashCommands() {
849867 }
850868
851869 const valueStr = value?.toString()?.trim();
852870 if (!valueStr && !allowEmpty) {
853871 if (!quiet) {
854872 toastr.error(t`No value provided for the secret key: ${key}`);
855873 }
@@ -857,7 +875,7 @@ function registerSecretSlashCommands() {
857875 }
858876
859877 const label = args?.label?.toString()?.trim() || getLabel();
860878 const id = await writeSecret(key, valueStr, label, { allowEmpty });
861879
862880 if (!quiet) {
863881 toastr.success(t`Secret has been written for the key: ${key}`);
src/endpoints/secrets.js+3 -2
@@ -561,12 +561,13 @@ router.post('/find', (request, response) => {
561561 }
562562
563563 const manager = new SecretManager(request.user.directories);
564564 const secretValuestate = manager.readSecretgetSecretState(key, id);
565565
566566 if (!secretValuestate[key]) {
567567 return response.sendStatus(404);
568568 }
569569
570+ const secretValue = manager.readSecret(key, id);
570571 return response.send({ value: secretValue });
571572 } catch (error) {
572573 console.error('Error finding secret:', error);