Merge branch 'staging' into persona-improvements

2cfaa034fbc69a6ed9c1a34315880b0d63e9e2bb

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

21 files changed, +171 -91Ignore whitespace
default/config.yaml+0 -2
@@ -77,8 +77,6 @@ perUserBasicAuth: false
7777## Set to 0 to expire session when the browser is closed
7878## Set to a negative number to disable session expiration
7979sessionTimeout: -1
80-# Used to sign session cookies. Will be auto-generated if not set
81-cookieSecret: ''
8280# Disable CSRF protection - NOT RECOMMENDED
8381disableCsrfProtection: false
8482# Disable startup security checks - NOT RECOMMENDED
post-install.js+19 -1
@@ -109,6 +109,15 @@ const keyMigrationMap = [
109109 newKey: 'logging.minLogLevel',
110110 migrate: (value) => value,
111111 },
112+ // uncomment one release after 1.12.13
113+ /*
114+ {
115+ oldKey: 'cookieSecret',
116+ newKey: 'cookieSecret',
117+ migrate: () => void 0,
118+ remove: true,
119+ },
120+ */
112121];
113122
114123/**
@@ -168,8 +177,17 @@ function addMissingConfigValues() {
168177
169178 // Migrate old keys to new keys
170179 const migratedKeys = [];
171180 for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) {
172181 if (_.has(config, oldKey)) {
182+ if (remove) {
183+ _.unset(config, oldKey);
184+ migratedKeys.push({
185+ oldKey,
186+ newValue: void 0,
187+ });
188+ continue;
189+ }
190+
173191 const oldValue = _.get(config, oldKey);
174192 const newValue = migrate(oldValue);
175193 _.set(config, newKey, newValue);
server.js+26 -25
@@ -261,47 +261,47 @@ app.use(responseTime());
261261
262262
263263/** @type {number} */
264264const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT, 'number');
265265/** @type {boolean} */
266266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN, 'boolean')) && !cliArguments.ssl;
267267/** @type {boolean} */
268268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN, 'boolean');
269269/** @type {string} */
270270const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271271/** @type {string} */
272272const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
273273/** @type {boolean} */
274274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY, 'boolean');
275275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST, 'boolean');
276276/** @type {string} */
277277const dataRootglobalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278278/** @type {boolean} */
279279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED, 'boolean');
280280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH, 'boolean');
281281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH, 'boolean');
282282/** @type {boolean} */
283283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS, 'boolean');
284284
285285const uploadsPath = path.join(dataRootglobalThis.DATA_ROOT, UPLOADS_DIRECTORY);
286286
287287
288288/** @type {boolean | "auto"string} */
289289let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6)) ?? DEFAULT_ENABLE_IPV6;
290290/** @type {boolean | "auto"string} */
291291let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4)) ?? DEFAULT_ENABLE_IPV4;
292292
293293/** @type {string} */
294294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295295/** @type {number} */
296296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT, 'number');
297297
298298/** @type {boolean} */
299299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6, 'boolean');
300300
301301/** @type {boolean} */
302302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST, 'boolean');
303303
304304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED, 'boolean');
305305const proxyUrl = cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', DEFAULT_PROXY_URL);
306306const proxyBypass = cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', DEFAULT_PROXY_BYPASS);
307307
@@ -403,7 +403,7 @@ if (enableCorsProxy) {
403403
404404function getSessionCookieAge() {
405405 // Defaults to "no expiration" if not set
406406 const configValue = getConfigValue('sessionTimeout', -1, 'number');
407407
408408 // Convert to milliseconds
409409 if (configValue > 0) {
@@ -474,7 +474,7 @@ app.use(cookieSession({
474474 sameSite: 'strict',
475475 httpOnly: true,
476476 maxAge: getSessionCookieAge(),
477477 secret: getCookieSecret(globalThis.DATA_ROOT),
478478}));
479479
480480app.use(setUserDataMiddleware);
@@ -884,8 +884,9 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
884884 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
885885 ));
886886 } else if (!perUserBasicAuth) {
887887 const basicAuthUserbasicAuthUserName = getConfigValue('basicAuthUser.username', {}'');
888- if (!basicAuthUser?.username || !basicAuthUser?.password) {
888+ const basicAuthUserPassword = getConfigValue('basicAuthUser.password', '');
889+ if (!basicAuthUserName || !basicAuthUserPassword) {
889890 console.warn(color.yellow(
890891 'Basic Authentication is enabled, but username or password is not set or empty!',
891892 ));
@@ -932,7 +933,7 @@ function setWindowTitle(title) {
932933function logSecurityAlert(message) {
933934 if (basicAuthMode || enableWhitelist) return; // safe!
934935 console.error(color.red(message));
935936 if (getConfigValue('securityOverride', false, 'boolean')) {
936937 console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
937938 return;
938939 }
@@ -1145,7 +1146,7 @@ function apply404Middleware() {
11451146}
11461147
11471148// User storage module needs to be initialized before starting the server
11481149initUserStorage(dataRootglobalThis.DATA_ROOT)
11491150 .then(ensurePublicDirectoriesExist)
11501151 .then(migrateUserData)
11511152 .then(migrateSystemPrompts)
src/endpoints/backends/chat-completions.js+4 -4
@@ -107,8 +107,8 @@ async function sendClaudeRequest(request, response) {
107107 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
108108 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
109109 const divider = '-'.repeat(process.stdout.columns);
110110 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean') && request.body.model.startsWith('claude-3');
111111 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
112112 // Disabled if not an integer or negative, or if the model doesn't support it
113113 if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0 || !request.body.model.startsWith('claude-3')) {
114114 cachingAtDepth = -1;
@@ -1004,7 +1004,7 @@ router.post('/generate', jsonParser, function (request, response) {
10041004 bodyParams.logprobs = true;
10051005 }
10061006
10071007 if (getConfigValue('openai.randomizeUserId', false, 'boolean')) {
10081008 bodyParams['user'] = uuidv4();
10091009 }
10101010 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
@@ -1040,7 +1040,7 @@ router.post('/generate', jsonParser, function (request, response) {
10401040 bodyParams['route'] = 'fallback';
10411041 }
10421042
10431043 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
10441044 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
10451045 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
10461046 }
src/endpoints/backends/text-completions.js+2 -2
@@ -372,8 +372,8 @@ router.post('/generate', jsonParser, async function (request, response) {
372372 }
373373
374374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
375375 const keepAlive = Number(getConfigValue('ollama.keepAlive', -1, 'number'));
376376 const numBatch = Number(getConfigValue('ollama.batchSize', -1, 'number'));
377377 if (numBatch > 0) {
378378 request.body['num_batch'] = numBatch;
379379 }
src/endpoints/characters.js+1 -1
@@ -24,7 +24,7 @@ import { importRisuSprites } from './sprites.js';
2424const defaultAvatarPath = './public/img/ai4.png';
2525
2626// KV-store for parsed character data
2727const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100, 'number')); // MB
2828// With 100 MB limit it would take roughly 3000 characters to reach this limit
2929const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity);
3030// Some Android devices require tighter memory management
src/endpoints/chats.js+3 -3
@@ -19,9 +19,9 @@ import {
1919 formatBytes,
2020} from '../util.js';
2121
2222const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean');
2323const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1, 'number'));
2424const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000, 'number'));
2525
2626/**
2727 * Saves a chat to the backups directory.
src/endpoints/content-manager.js+1 -1
@@ -165,7 +165,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
165165 */
166166export async function checkForNewContent(directoriesList, forceCategories = []) {
167167 try {
168168 const contentCheckSkip = getConfigValue('skipContentCheck', false, 'boolean');
169169 if (contentCheckSkip && forceCategories?.length === 0) {
170170 return;
171171 }
src/endpoints/secrets.js+2 -2
@@ -183,7 +183,7 @@ router.post('/read', jsonParser, (request, response) => {
183183});
184184
185185router.post('/view', jsonParser, async (request, response) => {
186186 const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean');
187187
188188 if (!allowKeysExposure) {
189189 console.error('secrets.json could not be viewed unless the value of allowKeysExposure in config.yaml is set to true');
@@ -205,7 +205,7 @@ router.post('/view', jsonParser, async (request, response) => {
205205});
206206
207207router.post('/find', jsonParser, (request, response) => {
208208 const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean');
209209 const key = request.body.key;
210210
211211 if (!allowKeysExposure && !EXPORTABLE_KEYS.includes(key)) {
src/endpoints/settings.js+3 -3
@@ -11,9 +11,9 @@ import { jsonParser } from '../express-common.js';
1111import { getAllUserHandles, getUserDirectories } from '../users.js';
1212import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1313
1414const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true, 'boolean');
1515const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true, 'boolean');
1616const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean');
1717
1818// 10 minutes
1919const AUTOSAVE_INTERVAL = 10 * 60 * 1000;
src/endpoints/thumbnails.js+6 -3
@@ -12,12 +12,15 @@ import { getAllUserHandles, getUserDirectories } from '../users.js';
1212import { getConfigValue } from '../util.js';
1313import { jsonParser } from '../express-common.js';
1414
1515const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean');
1616const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number'))));
1717const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png';
1818
1919/** @type {Record<string, number[]>} */
20-const dimensions = getConfigValue('thumbnails.dimensions', { 'bg': [160, 90], 'avatar': [96, 144] });
20+const dimensions = {
21+ 'bg': getConfigValue('thumbnails.dimensions.bg', [160, 90]),
22+ 'avatar': getConfigValue('thumbnails.dimensions.avatar', [96, 144]),
23+};
2124
2225/**
2326 * Gets a path to thumbnail folder based on the type.
src/endpoints/tokenizers.js+1 -1
@@ -56,7 +56,7 @@ export const TEXT_COMPLETION_MODELS = [
5656];
5757
5858const CHARS_PER_TOKEN = 3.35;
5959const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');
6060
6161/**
6262 * Gets a path to the tokenizer model. Downloads the model if it's a URL.
src/endpoints/users-public.js+2 -2
@@ -7,8 +7,8 @@ import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-co
77import { color, Cache, getConfigValue } from '../util.js';
88import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
99
1010const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false, 'boolean');
1111const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean');
1212const MFA_CACHE = new Cache(5 * 60 * 1000);
1313
1414const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
src/middleware/accessLogWriter.js+1 -1
@@ -3,7 +3,7 @@ import fs from 'node:fs';
33import { getRealIpFromHeader } from '../express-common.js';
44import { color, getConfigValue } from '../util.js';
55
66const enableAccessLog = getConfigValue('logging.enableAccessLog', true, 'boolean');
77
88const knownIPs = new Set();
99
src/middleware/basicAuth.js+6 -5
@@ -5,10 +5,10 @@
55import { Buffer } from 'node:buffer';
66import storage from 'node-persist';
77import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
88import { getConfig, getConfigValue, safeReadFileSync } from '../util.js';
99
1010const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean');
1111const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean');
1212
1313const basicAuthMiddleware = async function (request, response, callback) {
1414 const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? '';
@@ -17,7 +17,8 @@ const basicAuthMiddleware = async function (request, response, callback) {
1717 return res.status(401).send(unauthorizedWebpage);
1818 };
1919
2020 const configbasicAuthUserName = getConfiggetConfigValue('basicAuthUser.username');
21+ const basicAuthUserPassword = getConfigValue('basicAuthUser.password');
2122 const authHeader = request.headers.authorization;
2223
2324 if (!authHeader) {
@@ -35,7 +36,7 @@ const basicAuthMiddleware = async function (request, response, callback) {
3536 .toString('utf8')
3637 .split(':');
3738
3839 if (!usePerUserAuth && username === config.basicAuthUser.usernamebasicAuthUserName && password === config.basicAuthUser.passwordbasicAuthUserPassword) {
3940 return callback();
4041 } else if (usePerUserAuth) {
4142 const userHandles = await getAllUserHandles();
src/middleware/whitelist.js+1 -1
@@ -8,7 +8,7 @@ import { getIpFromRequest } from '../express-common.js';
88import { color, getConfigValue, safeReadFileSync } from '../util.js';
99
1010const whitelistPath = path.join(process.cwd(), './whitelist.txt');
1111const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean');
1212let whitelist = getConfigValue('whitelist', []);
1313
1414if (fs.existsSync(whitelistPath)) {
src/plugin-loader.js+2 -2
@@ -7,8 +7,8 @@ import { default as git, CheckRepoActions } from 'simple-git';
77import { sync as commandExistsSync } from 'command-exists';
88import { getConfigValue, color } from './util.js';
99
1010const enableServerPlugins = !!getConfigValue('enableServerPlugins', false, 'boolean');
1111const enableServerPluginsAutoUpdate = !!getConfigValue('enableServerPluginsAutoUpdate', true, 'boolean');
1212
1313/**
1414 * Map of loaded plugins.
src/prompt-converters.js+1 -1
@@ -572,7 +572,7 @@ export function convertMistralMessages(messages, names) {
572572 }
573573
574574 // Make the last assistant message a prefill
575575 const prefixEnabled = getConfigValue('mistral.enablePrefix', false, 'boolean');
576576 const lastMsg = messages[messages.length - 1];
577577 if (prefixEnabled && messages.length > 0 && lastMsg?.role === 'assistant') {
578578 lastMsg.prefix = true;
src/transformers.js+1 -1
@@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') {
132132
133133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');
134134 const model = forceModel || getModelForTask(task);
135135 const localOnly = !getConfigValue('extensions.models.autoDownload', true, 'boolean');
136136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
137137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
138138 tasks[task].pipeline = instance;
src/users.js+28 -13
@@ -15,15 +15,15 @@ import _ from 'lodash';
1515import { sync as writeFileAtomicSync } from 'write-file-atomic';
1616
1717import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js';
1818import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js';
1919import { readSecret, writeSecret } from './endpoints/secrets.js';
2020import { getContentOfType } from './endpoints/content-manager.js';
2121
2222export const KEY_PREFIX = 'user:';
2323const AVATAR_PREFIX = 'avatar:';
2424const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean');
2525const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false, 'boolean');
2626const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean');
2727const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2828
2929/**
@@ -32,9 +32,13 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
3232 */
3333const DIRECTORIES_CACHE = new Map();
3434const PUBLIC_USER_AVATAR = '/img/default-user.png';
35+const COOKIE_SECRET_PATH = 'cookie-secret.txt';
3536
3637const STORAGE_KEYS = {
3738 csrfSecret: 'csrfSecret',
39+ /**
40+ * @deprecated Read from COOKIE_SECRET_PATH in DATA_ROOT instead.
41+ */
3842 cookieSecret: 'cookieSecret',
3943};
4044
@@ -412,11 +416,10 @@ export function toAvatarKey(handle) {
412416 * @returns {Promise<void>}
413417 */
414418export async function initUserStorage(dataRoot) {
415- globalThis.DATA_ROOT = dataRoot;
419+ console.log('Using data root:', color.green(dataRoot));
416- console.log('Using data root:', color.green(globalThis.DATA_ROOT));
417420 console.log();
418421 await storage.init({
419422 dir: path.join(globalThis.DATA_ROOTdataRoot, '_storage'),
420423 ttl: false, // Never expire
421424 });
422425
@@ -430,17 +433,29 @@ export async function initUserStorage(dataRoot) {
430433
431434/**
432435 * Get the cookie secret from the config. If it doesn't exist, generate a new one.
436+ * @param {string} dataRoot The root directory for user data
433437 * @returns {string} The cookie secret
434438 */
435439export function getCookieSecret(dataRoot) {
436440 letconst secretcookieSecretPath = getConfigValue(STORAGE_KEYSpath.cookieSecretjoin(dataRoot, COOKIE_SECRET_PATH);
441+
442+ if (fs.existsSync(cookieSecretPath)) {
443+ const stat = fs.statSync(cookieSecretPath);
444+ if (stat.size > 0) {
445+ return fs.readFileSync(cookieSecretPath, 'utf8');
446+ }
447+ }
437448
438- if (!secret) {
449+ const oldSecret = getConfigValue(STORAGE_KEYS.cookieSecret);
439- console.warn(color.yellow('Cookie secret is missing from config.yaml. Generating a new one...'));
450+ if (oldSecret) {
440- secret = crypto.randomBytes(64).toString('base64');
451+ console.log('Migrating cookie secret from config.yaml...');
441- setConfigValue(STORAGE_KEYS.cookieSecret, secret);
452+ writeFileAtomicSync(cookieSecretPath, oldSecret, { encoding: 'utf8' });
453+ return oldSecret;
442454 }
443455
456+ console.warn(color.yellow('Cookie secret is missing from data root. Generating a new one...'));
457+ const secret = crypto.randomBytes(64).toString('base64');
458+ writeFileAtomicSync(cookieSecretPath, secret, { encoding: 'utf8' });
444459 return secret;
445460}
446461
src/util.js+61 -17
@@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns';
99
1010import yaml from 'yaml';
1111import { sync as commandExistsSync } from 'command-exists';
12-import { sync as writeFileAtomicSync } from 'write-file-atomic';
1312import _ from 'lodash';
1413import yauzl from 'yauzl';
1514import mime from 'mime-types';
@@ -22,6 +21,14 @@ import { LOG_LEVELS } from './constants.js';
2221let CACHED_CONFIG = null;
2322
2423/**
24+ * Converts a configuration key to an environment variable key.
25+ * @param {string} key Configuration key
26+ * @returns {string} Environment variable key
27+ * @example keyToEnv('extensions.models.speechToText') // 'SILLYTAVERN_EXTENSIONS_MODELS_SPEECHTOTEXT'
28+ */
29+export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_');
30+
31+/**
2532 * Returns the config object from the config.yaml file.
2633 * @returns {object} Config object
2734 */
@@ -51,24 +58,40 @@ export function getConfig() {
5158 * Returns the value for the given key from the config object.
5259 * @param {string} key - Key to get from the config object
5360 * @param {any} defaultValue - Default value to return if the key is not found
61+ * @param {'number'|'boolean'|null} typeConverter - Type to convert the value to
5462 * @returns {any} Value for the given key
5563 */
5664export function getConfigValue(key, defaultValue = null, typeConverter = null) {
57- const config = getConfig();
65+ function _getValue() {
58- return _.get(config, key, defaultValue);
66+ const envKey = keyToEnv(key);
67+ if (envKey in process.env) {
68+ const needsJsonParse = defaultValue && typeof defaultValue === 'object';
69+ const envValue = process.env[envKey];
70+ return needsJsonParse ? (tryParse(envValue) ?? defaultValue) : envValue;
71+ }
72+ const config = getConfig();
73+ return _.get(config, key, defaultValue);
74+ }
75+
76+ const value = _getValue();
77+ switch (typeConverter) {
78+ case 'number':
79+ return isNaN(parseFloat(value)) ? defaultValue : parseFloat(value);
80+ case 'boolean':
81+ return toBoolean(value);
82+ default:
83+ return value;
84+ }
5985}
6086
6187/**
62- * Sets a value for the given key in the config object and writes it to the config.yaml file.
88+ * THIS FUNCTION IS DEPRECATED AND ONLY EXISTS FOR BACKWARDS COMPATIBILITY. DON'T USE IT.
6389 * @param {stringany} key Key to_key setUnused
6490 * @param {any} value Value to_value setUnused
91+ * @deprecated Configs are read-only. Use environment variables instead.
6592 */
6693export function setConfigValue(key_key, value_value) {
67- // Reset cache so that the next getConfig call will read the updated config file
94+ console.trace(color.yellow('setConfigValue is deprecated and should not be used.'));
68- CACHED_CONFIG = null;
69- const config = getConfig();
70- _.set(config, key, value);
71- writeFileAtomicSync('./config.yaml', yaml.stringify(config));
7295}
7396
7497/**
@@ -394,7 +417,7 @@ export function generateTimestamp() {
394417 * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `backups.common.numberOfBackups` config value.
395418 */
396419export function removeOldBackups(directory, prefix, limit = null) {
397420 const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50, 'number'));
398421
399422 let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
400423 if (files.length > MAX_BACKUPS) {
@@ -747,6 +770,27 @@ export async function canResolve(name, useIPv6 = true, useIPv4 = true) {
747770 }
748771}
749772
773+/**
774+ * Converts various JavaScript primitives to boolean values.
775+ * Handles special case for "true"/"false" strings (case-insensitive)
776+ *
777+ * @param {any} value - The value to convert to boolean
778+ * @returns {boolean} - The boolean representation of the value
779+ */
780+export function toBoolean(value) {
781+ // Handle string values case-insensitively
782+ if (typeof value === 'string') {
783+ // Trim and convert to lowercase for case-insensitive comparison
784+ const trimmedLower = value.trim().toLowerCase();
785+
786+ // Handle explicit "true"/"false" strings
787+ if (trimmedLower === 'true') return true;
788+ if (trimmedLower === 'false') return false;
789+ }
790+
791+ // Handle all other JavaScript values based on their "truthiness"
792+ return Boolean(value);
793+}
750794
751795/**
752796 * converts string to boolean accepts 'true' or 'false' else it returns the string put in
@@ -754,8 +798,8 @@ export async function canResolve(name, useIPv6 = true, useIPv4 = true) {
754798 * @returns {boolean|string|null} boolean else original input string or null if input is
755799 */
756800export function stringToBool(str) {
757801 if (String(str).trim().toLowerCase() === 'true') return true;
758802 if (String(str).trim().toLowerCase() === 'false') return false;
759803 return str;
760804}
761805
@@ -763,7 +807,7 @@ export function stringToBool(str) {
763807 * Setup the minimum log level
764808 */
765809export function setupLogLevel() {
766810 const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG, 'number');
767811
768812 globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};
769813 globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};