From 7ea2c5f8cff0648be26f14eb6d68e9cd85a91678 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:16:44 +0200 Subject: [PATCH 01/13] Move cookie secret to data root. Make config.yaml immutable --- default/config.yaml | 2 -- post-install.js | 20 +++++++++++++++++++- server.js | 8 ++++---- src/users.js | 32 ++++++++++++++++++++++---------- src/util.js | 14 -------------- 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 5d7597a6c..d87537ad5 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -79,8 +79,6 @@ minLogLevel: 0 ## Set to 0 to expire session when the browser is closed ## Set to a negative number to disable session expiration sessionTimeout: -1 -# Used to sign session cookies. Will be auto-generated if not set -cookieSecret: '' # Disable CSRF protection - NOT RECOMMENDED disableCsrfProtection: false # Disable startup security checks - NOT RECOMMENDED diff --git a/post-install.js b/post-install.js index 46ad160cb..50c60e9e4 100644 --- a/post-install.js +++ b/post-install.js @@ -104,6 +104,15 @@ const keyMigrationMap = [ newKey: 'extensions.models.textToSpeech', migrate: (value) => value, }, + // uncommend one release after 1.12.13 + /* + { + oldKey: 'cookieSecret', + newKey: 'cookieSecret', + migrate: () => void 0, + remove: true, + }, + */ ]; /** @@ -163,8 +172,17 @@ function addMissingConfigValues() { // Migrate old keys to new keys const migratedKeys = []; - for (const { oldKey, newKey, migrate } of keyMigrationMap) { + for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) { if (_.has(config, oldKey)) { + if (remove) { + _.unset(config, oldKey); + migratedKeys.push({ + oldKey, + newValue: void 0, + }); + continue; + } + const oldValue = _.get(config, oldKey); const newValue = migrate(oldValue); _.set(config, newKey, newValue); diff --git a/server.js b/server.js index b83a79235..cdef0a64b 100644 --- a/server.js +++ b/server.js @@ -274,7 +274,7 @@ const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('list const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); /** @type {string} */ -const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); +globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); /** @type {boolean} */ const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); @@ -282,7 +282,7 @@ const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BAS /** @type {boolean} */ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); -const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); +const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); /** @type {boolean | "auto"} */ @@ -466,7 +466,7 @@ app.use(cookieSession({ sameSite: 'strict', httpOnly: true, maxAge: getSessionCookieAge(), - secret: getCookieSecret(), + secret: getCookieSecret(globalThis.DATA_ROOT), })); app.use(setUserDataMiddleware); @@ -1137,7 +1137,7 @@ function apply404Middleware() { } // User storage module needs to be initialized before starting the server -initUserStorage(dataRoot) +initUserStorage(globalThis.DATA_ROOT) .then(ensurePublicDirectoriesExist) .then(migrateUserData) .then(migrateSystemPrompts) diff --git a/src/users.js b/src/users.js index 2d7641ff2..7de379f25 100644 --- a/src/users.js +++ b/src/users.js @@ -15,7 +15,7 @@ import _ from 'lodash'; import { sync as writeFileAtomicSync } from 'write-file-atomic'; import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; -import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js'; +import { getConfigValue, color, delay, generateTimestamp } from './util.js'; import { readSecret, writeSecret } from './endpoints/secrets.js'; import { getContentOfType } from './endpoints/content-manager.js'; @@ -32,6 +32,7 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); */ const DIRECTORIES_CACHE = new Map(); const PUBLIC_USER_AVATAR = '/img/default-user.png'; +const COOKIE_SECRET_PATH = 'cookie-secret.txt'; const STORAGE_KEYS = { csrfSecret: 'csrfSecret', @@ -412,11 +413,10 @@ export function toAvatarKey(handle) { * @returns {Promise} */ export async function initUserStorage(dataRoot) { - globalThis.DATA_ROOT = dataRoot; - console.log('Using data root:', color.green(globalThis.DATA_ROOT)); + console.log('Using data root:', color.green(dataRoot)); console.log(); await storage.init({ - dir: path.join(globalThis.DATA_ROOT, '_storage'), + dir: path.join(dataRoot, '_storage'), ttl: false, // Never expire }); @@ -430,17 +430,29 @@ export async function initUserStorage(dataRoot) { /** * Get the cookie secret from the config. If it doesn't exist, generate a new one. + * @param {string} dataRoot The root directory for user data * @returns {string} The cookie secret */ -export function getCookieSecret() { - let secret = getConfigValue(STORAGE_KEYS.cookieSecret); +export function getCookieSecret(dataRoot) { + const cookieSecretPath = path.join(dataRoot, COOKIE_SECRET_PATH); - if (!secret) { - console.warn(color.yellow('Cookie secret is missing from config.yaml. Generating a new one...')); - secret = crypto.randomBytes(64).toString('base64'); - setConfigValue(STORAGE_KEYS.cookieSecret, secret); + if (fs.existsSync(cookieSecretPath)) { + const stat = fs.statSync(cookieSecretPath); + if (stat.size > 0) { + return fs.readFileSync(cookieSecretPath, 'utf8'); + } } + const oldSecret = getConfigValue(STORAGE_KEYS.cookieSecret); + if (oldSecret) { + console.log('Migrating cookie secret from config.yaml...'); + writeFileAtomicSync(cookieSecretPath, oldSecret, { encoding: 'utf8' }); + return oldSecret; + } + + console.warn(color.yellow('Cookie secret is missing from data root. Generating a new one...')); + const secret = crypto.randomBytes(64).toString('base64'); + writeFileAtomicSync(cookieSecretPath, secret, { encoding: 'utf8' }); return secret; } diff --git a/src/util.js b/src/util.js index 426b238ce..0ce6f66d4 100644 --- a/src/util.js +++ b/src/util.js @@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns'; import yaml from 'yaml'; import { sync as commandExistsSync } from 'command-exists'; -import { sync as writeFileAtomicSync } from 'write-file-atomic'; import _ from 'lodash'; import yauzl from 'yauzl'; import mime from 'mime-types'; @@ -58,19 +57,6 @@ export function getConfigValue(key, defaultValue = null) { return _.get(config, key, defaultValue); } -/** - * Sets a value for the given key in the config object and writes it to the config.yaml file. - * @param {string} key Key to set - * @param {any} value Value to set - */ -export function setConfigValue(key, value) { - // Reset cache so that the next getConfig call will read the updated config file - CACHED_CONFIG = null; - const config = getConfig(); - _.set(config, key, value); - writeFileAtomicSync('./config.yaml', yaml.stringify(config)); -} - /** * Encodes the Basic Auth header value for the given user and password. * @param {string} auth username:password From f6fe5fea77b96b5d490b6af8aecab52059fdc4fb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:29:42 +0200 Subject: [PATCH 02/13] Allow overriding config.yaml values with env vars Closes #3520 --- server.js | 2 +- src/util.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index cdef0a64b..933c10d5c 100644 --- a/server.js +++ b/server.js @@ -261,7 +261,7 @@ app.use(responseTime()); /** @type {number} */ -const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); +const server_port = cliArguments.port ?? getConfigValue('port', DEFAULT_PORT); /** @type {boolean} */ const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; /** @type {boolean} */ diff --git a/src/util.js b/src/util.js index 0ce6f66d4..27d6d2cf6 100644 --- a/src/util.js +++ b/src/util.js @@ -20,6 +20,14 @@ import { LOG_LEVELS } from './constants.js'; */ let CACHED_CONFIG = null; +/** + * Converts a configuration key to an environment variable key. + * @param {string} key Configuration key + * @returns {string} Environment variable key + * @example keyToEnv('extensions.models.speechToText') // 'SILLYTAVERN_EXTENSIONS_MODELS_SPEECHTOTEXT' + */ +export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_'); + /** * Returns the config object from the config.yaml file. * @returns {object} Config object @@ -53,6 +61,10 @@ export function getConfig() { * @returns {any} Value for the given key */ export function getConfigValue(key, defaultValue = null) { + const envKey = keyToEnv(key); + if (envKey in process.env) { + return process.env[envKey]; + } const config = getConfig(); return _.get(config, key, defaultValue); } From 7571552fefd62d752920eab3ce4a7821bd782472 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:45:20 +0200 Subject: [PATCH 03/13] STORAGE_KEYS.cookieSecret deprecated --- src/users.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/users.js b/src/users.js index 7de379f25..e7460fb1c 100644 --- a/src/users.js +++ b/src/users.js @@ -36,6 +36,9 @@ const COOKIE_SECRET_PATH = 'cookie-secret.txt'; const STORAGE_KEYS = { csrfSecret: 'csrfSecret', + /** + * @deprecated Read from COOKIE_SECRET_PATH in DATA_ROOT instead. + */ cookieSecret: 'cookieSecret', }; From 3f0393612558acb0f1783f0cf28cfaa6c09b494f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:53:48 +0200 Subject: [PATCH 04/13] Add config value type converters for numbers and booleans --- server.js | 30 ++++++------- src/endpoints/backends/chat-completions.js | 8 ++-- src/endpoints/backends/text-completions.js | 4 +- src/endpoints/characters.js | 2 +- src/endpoints/chats.js | 6 +-- src/endpoints/content-manager.js | 2 +- src/endpoints/secrets.js | 4 +- src/endpoints/settings.js | 6 +-- src/endpoints/thumbnails.js | 4 +- src/endpoints/tokenizers.js | 2 +- src/endpoints/users-public.js | 2 +- src/middleware/basicAuth.js | 4 +- src/middleware/whitelist.js | 2 +- src/plugin-loader.js | 4 +- src/prompt-converters.js | 2 +- src/transformers.js | 2 +- src/users.js | 6 +-- src/util.js | 51 ++++++++++++++++++---- 18 files changed, 88 insertions(+), 53 deletions(-) diff --git a/server.js b/server.js index 933c10d5c..4b4bc9274 100644 --- a/server.js +++ b/server.js @@ -261,26 +261,26 @@ app.use(responseTime()); /** @type {number} */ -const server_port = cliArguments.port ?? getConfigValue('port', DEFAULT_PORT); +const server_port = cliArguments.port ?? getConfigValue('port', DEFAULT_PORT, 'number'); /** @type {boolean} */ -const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; +const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN, 'boolean')) && !cliArguments.ssl; /** @type {boolean} */ -const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); +const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN, 'boolean'); /** @type {string} */ const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6); /** @type {string} */ const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4); /** @type {boolean} */ -const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); -const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); +const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY, 'boolean'); +const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST, 'boolean'); /** @type {string} */ globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); /** @type {boolean} */ -const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); -const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); -const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); +const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED, 'boolean'); +const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH, 'boolean'); +const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH, 'boolean'); /** @type {boolean} */ -const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); +const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS, 'boolean'); const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); @@ -293,15 +293,15 @@ let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protoc /** @type {string} */ const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); /** @type {number} */ -const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); +const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT, 'number'); /** @type {boolean} */ -const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); +const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6, 'boolean'); /** @type {boolean} */ -const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); +const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST, 'boolean'); -const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); +const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED, 'boolean'); const proxyUrl = cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', DEFAULT_PROXY_URL); const proxyBypass = cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', DEFAULT_PROXY_BYPASS); @@ -395,7 +395,7 @@ if (enableCorsProxy) { function getSessionCookieAge() { // Defaults to "no expiration" if not set - const configValue = getConfigValue('sessionTimeout', -1); + const configValue = getConfigValue('sessionTimeout', -1, 'number'); // Convert to milliseconds if (configValue > 0) { @@ -924,7 +924,7 @@ function setWindowTitle(title) { function logSecurityAlert(message) { if (basicAuthMode || enableWhitelist) return; // safe! console.error(color.red(message)); - if (getConfigValue('securityOverride', false)) { + if (getConfigValue('securityOverride', false, 'boolean')) { console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); return; } diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 17b1fce0a..144749dce 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -106,8 +106,8 @@ async function sendClaudeRequest(request, response) { const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString(); const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE); const divider = '-'.repeat(process.stdout.columns); - const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false) && request.body.model.startsWith('claude-3'); - let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1); + const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean') && request.body.model.startsWith('claude-3'); + let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); // Disabled if not an integer or negative, or if the model doesn't support it if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0 || !request.body.model.startsWith('claude-3')) { cachingAtDepth = -1; @@ -969,7 +969,7 @@ router.post('/generate', jsonParser, function (request, response) { bodyParams.logprobs = true; } - if (getConfigValue('openai.randomizeUserId', false)) { + if (getConfigValue('openai.randomizeUserId', false, 'boolean')) { bodyParams['user'] = uuidv4(); } } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { @@ -1008,7 +1008,7 @@ router.post('/generate', jsonParser, function (request, response) { bodyParams['include_reasoning'] = true; } - let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1); + let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) { cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth); } diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index c5da6280d..fa7cd089a 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -372,8 +372,8 @@ router.post('/generate', jsonParser, async function (request, response) { } if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { - const keepAlive = getConfigValue('ollama.keepAlive', -1); - const numBatch = getConfigValue('ollama.batchSize', -1); + const keepAlive = getConfigValue('ollama.keepAlive', -1, 'number'); + const numBatch = getConfigValue('ollama.batchSize', -1, 'number'); if (numBatch > 0) { request.body['num_batch'] = numBatch; } diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 294a195dc..8e6f831d9 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -24,7 +24,7 @@ import { importRisuSprites } from './sprites.js'; const defaultAvatarPath = './public/img/ai4.png'; // KV-store for parsed character data -const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100)); // MB +const cacheCapacity = getConfigValue('cardsCacheCapacity', 100, 'number'); // MB // With 100 MB limit it would take roughly 3000 characters to reach this limit const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity); // Some Android devices require tighter memory management diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index 803bb1ca5..e72542789 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -19,9 +19,9 @@ import { formatBytes, } from '../util.js'; -const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true); -const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1)); -const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000)); +const isBackupEnabled = getConfigValue('backups.chat.enabled', true, 'boolean'); +const maxTotalChatBackups = getConfigValue('backups.chat.maxTotalBackups', -1, 'number'); +const throttleInterval = getConfigValue('backups.chat.throttleInterval', 10_000, 'number'); /** * Saves a chat to the backups directory. diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js index 254134362..efc198aac 100644 --- a/src/endpoints/content-manager.js +++ b/src/endpoints/content-manager.js @@ -165,7 +165,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { */ export async function checkForNewContent(directoriesList, forceCategories = []) { try { - const contentCheckSkip = getConfigValue('skipContentCheck', false); + const contentCheckSkip = getConfigValue('skipContentCheck', false, 'boolean'); if (contentCheckSkip && forceCategories?.length === 0) { return; } diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index a7f0094ba..9f2c74769 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -183,7 +183,7 @@ router.post('/read', jsonParser, (request, response) => { }); router.post('/view', jsonParser, async (request, response) => { - const allowKeysExposure = getConfigValue('allowKeysExposure', false); + const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean'); if (!allowKeysExposure) { 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) => { }); router.post('/find', jsonParser, (request, response) => { - const allowKeysExposure = getConfigValue('allowKeysExposure', false); + const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean'); const key = request.body.key; if (!allowKeysExposure && !EXPORTABLE_KEYS.includes(key)) { diff --git a/src/endpoints/settings.js b/src/endpoints/settings.js index 887fd664b..de4afd36f 100644 --- a/src/endpoints/settings.js +++ b/src/endpoints/settings.js @@ -11,9 +11,9 @@ import { jsonParser } from '../express-common.js'; import { getAllUserHandles, getUserDirectories } from '../users.js'; import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; -const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); -const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); -const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); +const ENABLE_EXTENSIONS = getConfigValue('extensions.enabled', true, 'boolean'); +const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('extensions.autoUpdate', true, 'boolean'); +const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); // 10 minutes const AUTOSAVE_INTERVAL = 10 * 60 * 1000; diff --git a/src/endpoints/thumbnails.js b/src/endpoints/thumbnails.js index bba6a2a13..e67570157 100644 --- a/src/endpoints/thumbnails.js +++ b/src/endpoints/thumbnails.js @@ -12,8 +12,8 @@ import { getAllUserHandles, getUserDirectories } from '../users.js'; import { getConfigValue } from '../util.js'; import { jsonParser } from '../express-common.js'; -const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true); -const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95)))); +const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean'); +const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number')))); const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; /** @type {Record} */ diff --git a/src/endpoints/tokenizers.js b/src/endpoints/tokenizers.js index 505c932d9..f746d2e78 100644 --- a/src/endpoints/tokenizers.js +++ b/src/endpoints/tokenizers.js @@ -56,7 +56,7 @@ export const TEXT_COMPLETION_MODELS = [ ]; const CHARS_PER_TOKEN = 3.35; -const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true); +const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); /** * Gets a path to the tokenizer model. Downloads the model if it's a URL. diff --git a/src/endpoints/users-public.js b/src/endpoints/users-public.js index 8370e5748..4332b4b9b 100644 --- a/src/endpoints/users-public.js +++ b/src/endpoints/users-public.js @@ -7,7 +7,7 @@ import { jsonParser, getIpFromRequest } from '../express-common.js'; import { color, Cache, getConfigValue } from '../util.js'; import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; -const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false); +const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false, 'boolean'); const MFA_CACHE = new Cache(5 * 60 * 1000); export const router = express.Router(); diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index b75856289..910bfaeb7 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -7,8 +7,8 @@ import storage from 'node-persist'; import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; import { getConfig, getConfigValue, safeReadFileSync } from '../util.js'; -const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); -const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); +const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); +const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); const basicAuthMiddleware = async function (request, response, callback) { const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? ''; diff --git a/src/middleware/whitelist.js b/src/middleware/whitelist.js index cb90328ee..56bb3919c 100644 --- a/src/middleware/whitelist.js +++ b/src/middleware/whitelist.js @@ -8,7 +8,7 @@ import { getIpFromRequest } from '../express-common.js'; import { color, getConfigValue, safeReadFileSync } from '../util.js'; const whitelistPath = path.join(process.cwd(), './whitelist.txt'); -const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); +const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean'); let whitelist = getConfigValue('whitelist', []); let knownIPs = new Set(); diff --git a/src/plugin-loader.js b/src/plugin-loader.js index 744c3396d..221a6bc09 100644 --- a/src/plugin-loader.js +++ b/src/plugin-loader.js @@ -7,8 +7,8 @@ import { default as git } from 'simple-git'; import { sync as commandExistsSync } from 'command-exists'; import { getConfigValue, color } from './util.js'; -const enableServerPlugins = !!getConfigValue('enableServerPlugins', false); -const enableServerPluginsAutoUpdate = !!getConfigValue('enableServerPluginsAutoUpdate', true); +const enableServerPlugins = getConfigValue('enableServerPlugins', false, 'boolean'); +const enableServerPluginsAutoUpdate = getConfigValue('enableServerPluginsAutoUpdate', true, 'boolean'); /** * Map of loaded plugins. diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 4389b7199..f432e8a04 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -572,7 +572,7 @@ export function convertMistralMessages(messages, names) { } // Make the last assistant message a prefill - const prefixEnabled = getConfigValue('mistral.enablePrefix', false); + const prefixEnabled = getConfigValue('mistral.enablePrefix', false, 'boolean'); const lastMsg = messages[messages.length - 1]; if (prefixEnabled && messages.length > 0 && lastMsg?.role === 'assistant') { lastMsg.prefix = true; diff --git a/src/transformers.js b/src/transformers.js index c8f8da4f6..2bd03f4a5 100644 --- a/src/transformers.js +++ b/src/transformers.js @@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') { const cacheDir = path.join(globalThis.DATA_ROOT, '_cache'); const model = forceModel || getModelForTask(task); - const localOnly = !getConfigValue('extensions.models.autoDownload', true); + const localOnly = !getConfigValue('extensions.models.autoDownload', true, 'boolean'); console.log('Initializing transformers.js pipeline for task', task, 'with model', model); const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly }); tasks[task].pipeline = instance; diff --git a/src/users.js b/src/users.js index e7460fb1c..b02c6e935 100644 --- a/src/users.js +++ b/src/users.js @@ -21,9 +21,9 @@ import { getContentOfType } from './endpoints/content-manager.js'; export const KEY_PREFIX = 'user:'; const AVATAR_PREFIX = 'avatar:'; -const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); -const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false); -const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); +const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); +const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false, 'boolean'); +const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); /** diff --git a/src/util.js b/src/util.js index 27d6d2cf6..e1bbd53fd 100644 --- a/src/util.js +++ b/src/util.js @@ -54,19 +54,33 @@ export function getConfig() { } } + /** * Returns the value for the given key from the config object. * @param {string} key - Key to get from the config object * @param {any} defaultValue - Default value to return if the key is not found + * @param {'number'|'boolean'|null} typeConverter - Type to convert the value to * @returns {any} Value for the given key */ -export function getConfigValue(key, defaultValue = null) { - const envKey = keyToEnv(key); - if (envKey in process.env) { - return process.env[envKey]; +export function getConfigValue(key, defaultValue = null, typeConverter = null) { + function _getValue() { + const envKey = keyToEnv(key); + if (envKey in process.env) { + return process.env[envKey]; + } + const config = getConfig(); + return _.get(config, key, defaultValue); + } + + const value = _getValue(); + switch (typeConverter) { + case 'number': + return Number(value); + case 'boolean': + return toBoolean(value); + default: + return value; } - const config = getConfig(); - return _.get(config, key, defaultValue); } /** @@ -392,7 +406,7 @@ export function generateTimestamp() { * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `backups.common.numberOfBackups` config value. */ export function removeOldBackups(directory, prefix, limit = null) { - const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50)); + const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50, 'number')); let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); if (files.length > MAX_BACKUPS) { @@ -745,6 +759,27 @@ export async function canResolve(name, useIPv6 = true, useIPv4 = true) { } } +/** + * Converts various JavaScript primitives to boolean values. + * Handles special case for "true"/"false" strings (case-insensitive) + * + * @param {any} value - The value to convert to boolean + * @returns {boolean} - The boolean representation of the value + */ +export function toBoolean(value) { + // Handle string values case-insensitively + if (typeof value === 'string') { + // Trim and convert to lowercase for case-insensitive comparison + const trimmedLower = value.trim().toLowerCase(); + + // Handle explicit "true"/"false" strings + if (trimmedLower === 'true') return true; + if (trimmedLower === 'false') return false; + } + + // Handle all other JavaScript values based on their "truthiness" + return Boolean(value); +} /** * converts string to boolean accepts 'true' or 'false' else it returns the string put in @@ -761,7 +796,7 @@ export function stringToBool(str) { * Setup the minimum log level */ export function setupLogLevel() { - const logLevel = getConfigValue('minLogLevel', LOG_LEVELS.DEBUG); + const logLevel = getConfigValue('minLogLevel', LOG_LEVELS.DEBUG, 'number'); globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {}; globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {}; From 2b28065c9f4a17367b2fc54e3577d257d986c92d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:28:30 +0200 Subject: [PATCH 05/13] Fix setting protocols with env --- server.js | 8 ++++---- src/util.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index 4b4bc9274..af4578a50 100644 --- a/server.js +++ b/server.js @@ -285,10 +285,10 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS, 'b const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); -/** @type {boolean | "auto"} */ -let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); -/** @type {boolean | "auto"} */ -let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); +/** @type {boolean | string} */ +let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6)) ?? DEFAULT_ENABLE_IPV6; +/** @type {boolean | string} */ +let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4)) ?? DEFAULT_ENABLE_IPV4; /** @type {string} */ const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); diff --git a/src/util.js b/src/util.js index e1bbd53fd..98dac92bc 100644 --- a/src/util.js +++ b/src/util.js @@ -787,8 +787,8 @@ export function toBoolean(value) { * @returns {boolean|string|null} boolean else original input string or null if input is */ export function stringToBool(str) { - if (str === 'true') return true; - if (str === 'false') return false; + if (String(str).trim().toLowerCase() === 'true') return true; + if (String(str).trim().toLowerCase() === 'false') return false; return str; } From 73784642d27e9d877c069f9e25464144ea5ebc9f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:31:23 +0200 Subject: [PATCH 06/13] Save setConfigValue export --- src/util.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index 98dac92bc..e977c4702 100644 --- a/src/util.js +++ b/src/util.js @@ -54,7 +54,6 @@ export function getConfig() { } } - /** * Returns the value for the given key from the config object. * @param {string} key - Key to get from the config object @@ -83,6 +82,16 @@ export function getConfigValue(key, defaultValue = null, typeConverter = null) { } } +/** + * THIS FUNCTION IS DEPRECATED AND ONLY EXISTS FOR BACKWARDS COMPATIBILITY. DON'T USE IT. + * @param {any} _key Unused + * @param {any} _value Unused + * @deprecated Configs are read-only. Use environment variables instead. + */ +export function setConfigValue(_key, _value) { + console.trace(color.yellow('setConfigValue is deprecated and should not be used.')); +} + /** * Encodes the Basic Auth header value for the given user and password. * @param {string} auth username:password From 00bb36f764e7fe232043347978badf32cc1cd639 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:38:47 +0200 Subject: [PATCH 07/13] Fix setting basic auth creds with env --- server.js | 5 +++-- src/middleware/basicAuth.js | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index af4578a50..d1516e71f 100644 --- a/server.js +++ b/server.js @@ -876,8 +876,9 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.', )); } else if (!perUserBasicAuth) { - const basicAuthUser = getConfigValue('basicAuthUser', {}); - if (!basicAuthUser?.username || !basicAuthUser?.password) { + const basicAuthUserName = getConfigValue('basicAuthUser.username', ''); + const basicAuthUserPassword = getConfigValue('basicAuthUser.password', ''); + if (!basicAuthUserName || !basicAuthUserPassword) { console.warn(color.yellow( 'Basic Authentication is enabled, but username or password is not set or empty!', )); diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index 910bfaeb7..83f952398 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'; import storage from 'node-persist'; import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; -import { getConfig, getConfigValue, safeReadFileSync } from '../util.js'; +import { getConfigValue, safeReadFileSync } from '../util.js'; const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); @@ -17,7 +17,8 @@ const basicAuthMiddleware = async function (request, response, callback) { return res.status(401).send(unauthorizedWebpage); }; - const config = getConfig(); + const basicAuthUserName = getConfigValue('basicAuthUser.username'); + const basicAuthUserPassword = getConfigValue('basicAuthUser.password'); const authHeader = request.headers.authorization; if (!authHeader) { @@ -35,7 +36,7 @@ const basicAuthMiddleware = async function (request, response, callback) { .toString('utf8') .split(':'); - if (!usePerUserAuth && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { + if (!usePerUserAuth && username === basicAuthUserName && password === basicAuthUserPassword) { return callback(); } else if (usePerUserAuth) { const userHandles = await getAllUserHandles(); From 252ae9f5340e29c4c642806ba2dc9758e5d080c7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 22 Feb 2025 20:20:23 +0200 Subject: [PATCH 08/13] Update access log configuration to enforce boolean type --- src/middleware/accessLogWriter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/accessLogWriter.js b/src/middleware/accessLogWriter.js index 26a2b49aa..4aaeeb5a6 100644 --- a/src/middleware/accessLogWriter.js +++ b/src/middleware/accessLogWriter.js @@ -3,7 +3,7 @@ import fs from 'node:fs'; import { getRealIpFromHeader } from '../express-common.js'; import { color, getConfigValue } from '../util.js'; -const enableAccessLog = getConfigValue('logging.enableAccessLog', true); +const enableAccessLog = getConfigValue('logging.enableAccessLog', true, 'boolean'); const knownIPs = new Set(); From 861c502e4472be977f608adb1b0f850c312143ef Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:12:22 +0200 Subject: [PATCH 09/13] Return type casts where they were --- src/endpoints/backends/text-completions.js | 4 ++-- src/endpoints/characters.js | 2 +- src/endpoints/chats.js | 6 +++--- src/endpoints/settings.js | 6 +++--- src/plugin-loader.js | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index fa7cd089a..618bad832 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -372,8 +372,8 @@ router.post('/generate', jsonParser, async function (request, response) { } if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { - const keepAlive = getConfigValue('ollama.keepAlive', -1, 'number'); - const numBatch = getConfigValue('ollama.batchSize', -1, 'number'); + const keepAlive = Number(getConfigValue('ollama.keepAlive', -1, 'number')); + const numBatch = Number(getConfigValue('ollama.batchSize', -1, 'number')); if (numBatch > 0) { request.body['num_batch'] = numBatch; } diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 8e6f831d9..ca99bf266 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -24,7 +24,7 @@ import { importRisuSprites } from './sprites.js'; const defaultAvatarPath = './public/img/ai4.png'; // KV-store for parsed character data -const cacheCapacity = getConfigValue('cardsCacheCapacity', 100, 'number'); // MB +const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100, 'number')); // MB // With 100 MB limit it would take roughly 3000 characters to reach this limit const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity); // Some Android devices require tighter memory management diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index e72542789..a237b3109 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -19,9 +19,9 @@ import { formatBytes, } from '../util.js'; -const isBackupEnabled = getConfigValue('backups.chat.enabled', true, 'boolean'); -const maxTotalChatBackups = getConfigValue('backups.chat.maxTotalBackups', -1, 'number'); -const throttleInterval = getConfigValue('backups.chat.throttleInterval', 10_000, 'number'); +const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean'); +const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1, 'number')); +const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000, 'number')); /** * Saves a chat to the backups directory. diff --git a/src/endpoints/settings.js b/src/endpoints/settings.js index de4afd36f..2f31b82f3 100644 --- a/src/endpoints/settings.js +++ b/src/endpoints/settings.js @@ -11,9 +11,9 @@ import { jsonParser } from '../express-common.js'; import { getAllUserHandles, getUserDirectories } from '../users.js'; import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; -const ENABLE_EXTENSIONS = getConfigValue('extensions.enabled', true, 'boolean'); -const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('extensions.autoUpdate', true, 'boolean'); -const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); +const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true, 'boolean'); +const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true, 'boolean'); +const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean'); // 10 minutes const AUTOSAVE_INTERVAL = 10 * 60 * 1000; diff --git a/src/plugin-loader.js b/src/plugin-loader.js index ed497c257..7d7053025 100644 --- a/src/plugin-loader.js +++ b/src/plugin-loader.js @@ -7,8 +7,8 @@ import { default as git, CheckRepoActions } from 'simple-git'; import { sync as commandExistsSync } from 'command-exists'; import { getConfigValue, color } from './util.js'; -const enableServerPlugins = getConfigValue('enableServerPlugins', false, 'boolean'); -const enableServerPluginsAutoUpdate = getConfigValue('enableServerPluginsAutoUpdate', true, 'boolean'); +const enableServerPlugins = !!getConfigValue('enableServerPlugins', false, 'boolean'); +const enableServerPluginsAutoUpdate = !!getConfigValue('enableServerPluginsAutoUpdate', true, 'boolean'); /** * Map of loaded plugins. From 4edb069bd80bcc1672d3f05b9119d8e1303bf9f1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:19:09 +0200 Subject: [PATCH 10/13] Split avatar dimensions config read by types --- src/endpoints/thumbnails.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/endpoints/thumbnails.js b/src/endpoints/thumbnails.js index e67570157..4d1b8b4a2 100644 --- a/src/endpoints/thumbnails.js +++ b/src/endpoints/thumbnails.js @@ -17,7 +17,10 @@ const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.qu const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; /** @type {Record} */ -const dimensions = getConfigValue('thumbnails.dimensions', { 'bg': [160, 90], 'avatar': [96, 144] }); +const dimensions = { + 'bg': getConfigValue('thumbnails.dimensions.bg', [160, 90]), + 'avatar': getConfigValue('thumbnails.dimensions.avatar', [96, 144]), +}; /** * Gets a path to thumbnail folder based on the type. From 0776f65193dcc252c17fe3fc85859905198532da Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:20:17 +0200 Subject: [PATCH 11/13] Allow JSON array and objects in env.var configs --- src/util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index 7235b52bc..17356c985 100644 --- a/src/util.js +++ b/src/util.js @@ -65,7 +65,9 @@ export function getConfigValue(key, defaultValue = null, typeConverter = null) { function _getValue() { const envKey = keyToEnv(key); if (envKey in process.env) { - return process.env[envKey]; + const needsJsonParse = defaultValue && typeof defaultValue === 'object'; + const envValue = process.env[envKey]; + return needsJsonParse ? (tryParse(envValue) ?? defaultValue) : envValue; } const config = getConfig(); return _.get(config, key, defaultValue); From acd8b817f4957839649de957383353382d21c00c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:08:04 +0200 Subject: [PATCH 12/13] Improve number type conversion in getConfigValue to handle NaN cases --- src/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index 17356c985..e98eb4978 100644 --- a/src/util.js +++ b/src/util.js @@ -76,7 +76,7 @@ export function getConfigValue(key, defaultValue = null, typeConverter = null) { const value = _getValue(); switch (typeConverter) { case 'number': - return Number(value); + return isNaN(parseFloat(value)) ? defaultValue : parseFloat(value); case 'boolean': return toBoolean(value); default: From 4aa867292564bdf07bc6615e8d5c4fb743a04f69 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:08:59 +0200 Subject: [PATCH 13/13] Fix typo in post-install.js comment --- post-install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/post-install.js b/post-install.js index d2782224d..00ef731ad 100644 --- a/post-install.js +++ b/post-install.js @@ -109,7 +109,7 @@ const keyMigrationMap = [ newKey: 'logging.minLogLevel', migrate: (value) => value, }, - // uncommend one release after 1.12.13 + // uncomment one release after 1.12.13 /* { oldKey: 'cookieSecret',