Merge pull request #3521 from SillyTavern/immutable-config Immutable config
Signed| @@ -77,8 +77,6 @@ perUserBasicAuth: false | |||
| 77 | ## Set to 0 to expire session when the browser is closed | 77 | ## Set to 0 to expire session when the browser is closed |
| 78 | ## Set to a negative number to disable session expiration | 78 | ## Set to a negative number to disable session expiration |
| 79 | sessionTimeout: -1 | 79 | sessionTimeout: -1 |
| 80 | # Used to sign session cookies. Will be auto-generated if not set | ||
| 81 | cookieSecret: '' | ||
| 82 | # Disable CSRF protection - NOT RECOMMENDED | 80 | # Disable CSRF protection - NOT RECOMMENDED |
| 83 | disableCsrfProtection: false | 81 | disableCsrfProtection: false |
| 84 | # Disable startup security checks - NOT RECOMMENDED | 82 | # Disable startup security checks - NOT RECOMMENDED |
| @@ -109,6 +109,15 @@ const keyMigrationMap = [ | |||
| 109 | newKey: 'logging.minLogLevel', | 109 | newKey: 'logging.minLogLevel', |
| 110 | migrate: (value) => value, | 110 | migrate: (value) => value, |
| 111 | }, | 111 | }, |
| 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 | */ | ||
| 112 | ]; | 121 | ]; |
| 113 | 122 | ||
| 114 | /** | 123 | /** |
| @@ -168,8 +177,17 @@ function addMissingConfigValues() { | |||
| 168 | 177 | ||
| 169 | // Migrate old keys to new keys | 178 | // Migrate old keys to new keys |
| 170 | const migratedKeys = []; | 179 | const migratedKeys = []; |
| 171 | for (const { oldKey, newKey, migrate } of keyMigrationMap) { | 180 | for (const { oldKey, newKey, migrate, remove } of keyMigrationMap) { |
| 172 | if (_.has(config, oldKey)) { | 181 | 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 | |||
| 173 | const oldValue = _.get(config, oldKey); | 191 | const oldValue = _.get(config, oldKey); |
| 174 | const newValue = migrate(oldValue); | 192 | const newValue = migrate(oldValue); |
| 175 | _.set(config, newKey, newValue); | 193 | _.set(config, newKey, newValue); |
| @@ -261,47 +261,47 @@ app.use(responseTime()); | |||
| 261 | 261 | ||
| 262 | 262 | ||
| 263 | /** @type {number} */ | 263 | /** @type {number} */ |
| 264 | const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); | 264 | const server_port = cliArguments.port ?? getConfigValue('port', DEFAULT_PORT, 'number'); |
| 265 | /** @type {boolean} */ | 265 | /** @type {boolean} */ |
| 266 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; | 266 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN, 'boolean')) && !cliArguments.ssl; |
| 267 | /** @type {boolean} */ | 267 | /** @type {boolean} */ |
| 268 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); | 268 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN, 'boolean'); |
| 269 | /** @type {string} */ | 269 | /** @type {string} */ |
| 270 | const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6); | 270 | const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6); |
| 271 | /** @type {string} */ | 271 | /** @type {string} */ |
| 272 | const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4); | 272 | const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4); |
| 273 | /** @type {boolean} */ | 273 | /** @type {boolean} */ |
| 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); | 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY, 'boolean'); |
| 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); | 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST, 'boolean'); |
| 276 | /** @type {string} */ | 276 | /** @type {string} */ |
| 277 | const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); | 277 | globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); |
| 278 | /** @type {boolean} */ | 278 | /** @type {boolean} */ |
| 279 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED); | 279 | const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED, 'boolean'); |
| 280 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH); | 280 | const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH, 'boolean'); |
| 281 | const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH); | 281 | const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH, 'boolean'); |
| 282 | /** @type {boolean} */ | 282 | /** @type {boolean} */ |
| 283 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS); | 283 | const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS, 'boolean'); |
| 284 | 284 | ||
| 285 | const uploadsPath = path.join(dataRoot, UPLOADS_DIRECTORY); | 285 | const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY); |
| 286 | 286 | ||
| 287 | 287 | ||
| 288 | /** @type {boolean | "auto"} */ | 288 | /** @type {boolean | string} */ |
| 289 | let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6); | 289 | let enableIPv6 = stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6)) ?? DEFAULT_ENABLE_IPV6; |
| 290 | /** @type {boolean | "auto"} */ | 290 | /** @type {boolean | string} */ |
| 291 | let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4); | 291 | let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4)) ?? DEFAULT_ENABLE_IPV4; |
| 292 | 292 | ||
| 293 | /** @type {string} */ | 293 | /** @type {string} */ |
| 294 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); | 294 | const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME); |
| 295 | /** @type {number} */ | 295 | /** @type {number} */ |
| 296 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT); | 296 | const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT, 'number'); |
| 297 | 297 | ||
| 298 | /** @type {boolean} */ | 298 | /** @type {boolean} */ |
| 299 | const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6); | 299 | const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6, 'boolean'); |
| 300 | 300 | ||
| 301 | /** @type {boolean} */ | 301 | /** @type {boolean} */ |
| 302 | const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST); | 302 | const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST, 'boolean'); |
| 303 | 303 | ||
| 304 | const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED); | 304 | const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED, 'boolean'); |
| 305 | const proxyUrl = cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', DEFAULT_PROXY_URL); | 305 | const proxyUrl = cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', DEFAULT_PROXY_URL); |
| 306 | const proxyBypass = cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', DEFAULT_PROXY_BYPASS); | 306 | const proxyBypass = cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', DEFAULT_PROXY_BYPASS); |
| 307 | 307 | ||
| @@ -403,7 +403,7 @@ if (enableCorsProxy) { | |||
| 403 | 403 | ||
| 404 | function getSessionCookieAge() { | 404 | function getSessionCookieAge() { |
| 405 | // Defaults to "no expiration" if not set | 405 | // Defaults to "no expiration" if not set |
| 406 | const configValue = getConfigValue('sessionTimeout', -1); | 406 | const configValue = getConfigValue('sessionTimeout', -1, 'number'); |
| 407 | 407 | ||
| 408 | // Convert to milliseconds | 408 | // Convert to milliseconds |
| 409 | if (configValue > 0) { | 409 | if (configValue > 0) { |
| @@ -474,7 +474,7 @@ app.use(cookieSession({ | |||
| 474 | sameSite: 'strict', | 474 | sameSite: 'strict', |
| 475 | httpOnly: true, | 475 | httpOnly: true, |
| 476 | maxAge: getSessionCookieAge(), | 476 | maxAge: getSessionCookieAge(), |
| 477 | secret: getCookieSecret(), | 477 | secret: getCookieSecret(globalThis.DATA_ROOT), |
| 478 | })); | 478 | })); |
| 479 | 479 | ||
| 480 | app.use(setUserDataMiddleware); | 480 | app.use(setUserDataMiddleware); |
| @@ -884,8 +884,9 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { | |||
| 884 | 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.', | 884 | 'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.', |
| 885 | )); | 885 | )); |
| 886 | } else if (!perUserBasicAuth) { | 886 | } else if (!perUserBasicAuth) { |
| 887 | const basicAuthUser = getConfigValue('basicAuthUser', {}); | 887 | const basicAuthUserName = getConfigValue('basicAuthUser.username', ''); |
| 888 | if (!basicAuthUser?.username || !basicAuthUser?.password) { | 888 | const basicAuthUserPassword = getConfigValue('basicAuthUser.password', ''); |
| 889 | if (!basicAuthUserName || !basicAuthUserPassword) { | ||
| 889 | console.warn(color.yellow( | 890 | console.warn(color.yellow( |
| 890 | 'Basic Authentication is enabled, but username or password is not set or empty!', | 891 | 'Basic Authentication is enabled, but username or password is not set or empty!', |
| 891 | )); | 892 | )); |
| @@ -932,7 +933,7 @@ function setWindowTitle(title) { | |||
| 932 | function logSecurityAlert(message) { | 933 | function logSecurityAlert(message) { |
| 933 | if (basicAuthMode || enableWhitelist) return; // safe! | 934 | if (basicAuthMode || enableWhitelist) return; // safe! |
| 934 | console.error(color.red(message)); | 935 | console.error(color.red(message)); |
| 935 | if (getConfigValue('securityOverride', false)) { | 936 | if (getConfigValue('securityOverride', false, 'boolean')) { |
| 936 | console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); | 937 | console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); |
| 937 | return; | 938 | return; |
| 938 | } | 939 | } |
| @@ -1145,7 +1146,7 @@ function apply404Middleware() { | |||
| 1145 | } | 1146 | } |
| 1146 | 1147 | ||
| 1147 | // User storage module needs to be initialized before starting the server | 1148 | // User storage module needs to be initialized before starting the server |
| 1148 | initUserStorage(dataRoot) | 1149 | initUserStorage(globalThis.DATA_ROOT) |
| 1149 | .then(ensurePublicDirectoriesExist) | 1150 | .then(ensurePublicDirectoriesExist) |
| 1150 | .then(migrateUserData) | 1151 | .then(migrateUserData) |
| 1151 | .then(migrateSystemPrompts) | 1152 | .then(migrateSystemPrompts) |
| @@ -107,8 +107,8 @@ async function sendClaudeRequest(request, response) { | |||
| 107 | const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString(); | 107 | const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString(); |
| 108 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE); | 108 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE); |
| 109 | const divider = '-'.repeat(process.stdout.columns); | 109 | const divider = '-'.repeat(process.stdout.columns); |
| 110 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false) && request.body.model.startsWith('claude-3'); | 110 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean') && request.body.model.startsWith('claude-3'); |
| 111 | let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1); | 111 | let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 112 | // Disabled if not an integer or negative, or if the model doesn't support it | 112 | // Disabled if not an integer or negative, or if the model doesn't support it |
| 113 | if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0 || !request.body.model.startsWith('claude-3')) { | 113 | if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0 || !request.body.model.startsWith('claude-3')) { |
| 114 | cachingAtDepth = -1; | 114 | cachingAtDepth = -1; |
| @@ -1004,7 +1004,7 @@ router.post('/generate', jsonParser, function (request, response) { | |||
| 1004 | bodyParams.logprobs = true; | 1004 | bodyParams.logprobs = true; |
| 1005 | } | 1005 | } |
| 1006 | 1006 | ||
| 1007 | if (getConfigValue('openai.randomizeUserId', false)) { | 1007 | if (getConfigValue('openai.randomizeUserId', false, 'boolean')) { |
| 1008 | bodyParams['user'] = uuidv4(); | 1008 | bodyParams['user'] = uuidv4(); |
| 1009 | } | 1009 | } |
| 1010 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { | 1010 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { |
| @@ -1040,7 +1040,7 @@ router.post('/generate', jsonParser, function (request, response) { | |||
| 1040 | bodyParams['route'] = 'fallback'; | 1040 | bodyParams['route'] = 'fallback'; |
| 1041 | } | 1041 | } |
| 1042 | 1042 | ||
| 1043 | let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1); | 1043 | let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 1044 | if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) { | 1044 | if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) { |
| 1045 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth); | 1045 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth); |
| 1046 | } | 1046 | } |
| @@ -372,8 +372,8 @@ router.post('/generate', jsonParser, async function (request, response) { | |||
| 372 | } | 372 | } |
| 373 | 373 | ||
| 374 | if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { | 374 | if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { |
| 375 | const keepAlive = getConfigValue('ollama.keepAlive', -1); | 375 | const keepAlive = Number(getConfigValue('ollama.keepAlive', -1, 'number')); |
| 376 | const numBatch = getConfigValue('ollama.batchSize', -1); | 376 | const numBatch = Number(getConfigValue('ollama.batchSize', -1, 'number')); |
| 377 | if (numBatch > 0) { | 377 | if (numBatch > 0) { |
| 378 | request.body['num_batch'] = numBatch; | 378 | request.body['num_batch'] = numBatch; |
| 379 | } | 379 | } |
| @@ -24,7 +24,7 @@ import { importRisuSprites } from './sprites.js'; | |||
| 24 | const defaultAvatarPath = './public/img/ai4.png'; | 24 | const defaultAvatarPath = './public/img/ai4.png'; |
| 25 | 25 | ||
| 26 | // KV-store for parsed character data | 26 | // KV-store for parsed character data |
| 27 | const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100)); // MB | 27 | const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100, 'number')); // MB |
| 28 | // With 100 MB limit it would take roughly 3000 characters to reach this limit | 28 | // With 100 MB limit it would take roughly 3000 characters to reach this limit |
| 29 | const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity); | 29 | const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity); |
| 30 | // Some Android devices require tighter memory management | 30 | // Some Android devices require tighter memory management |
| @@ -19,9 +19,9 @@ import { | |||
| 19 | formatBytes, | 19 | formatBytes, |
| 20 | } from '../util.js'; | 20 | } from '../util.js'; |
| 21 | 21 | ||
| 22 | const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true); | 22 | const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean'); |
| 23 | const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1)); | 23 | const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1, 'number')); |
| 24 | const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000)); | 24 | const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000, 'number')); |
| 25 | 25 | ||
| 26 | /** | 26 | /** |
| 27 | * Saves a chat to the backups directory. | 27 | * Saves a chat to the backups directory. |
| @@ -165,7 +165,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { | |||
| 165 | */ | 165 | */ |
| 166 | export async function checkForNewContent(directoriesList, forceCategories = []) { | 166 | export async function checkForNewContent(directoriesList, forceCategories = []) { |
| 167 | try { | 167 | try { |
| 168 | const contentCheckSkip = getConfigValue('skipContentCheck', false); | 168 | const contentCheckSkip = getConfigValue('skipContentCheck', false, 'boolean'); |
| 169 | if (contentCheckSkip && forceCategories?.length === 0) { | 169 | if (contentCheckSkip && forceCategories?.length === 0) { |
| 170 | return; | 170 | return; |
| 171 | } | 171 | } |
| @@ -183,7 +183,7 @@ router.post('/read', jsonParser, (request, response) => { | |||
| 183 | }); | 183 | }); |
| 184 | 184 | ||
| 185 | router.post('/view', jsonParser, async (request, response) => { | 185 | router.post('/view', jsonParser, async (request, response) => { |
| 186 | const allowKeysExposure = getConfigValue('allowKeysExposure', false); | 186 | const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean'); |
| 187 | 187 | ||
| 188 | if (!allowKeysExposure) { | 188 | if (!allowKeysExposure) { |
| 189 | console.error('secrets.json could not be viewed unless the value of allowKeysExposure in config.yaml is set to true'); | 189 | 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) => { | |||
| 205 | }); | 205 | }); |
| 206 | 206 | ||
| 207 | router.post('/find', jsonParser, (request, response) => { | 207 | router.post('/find', jsonParser, (request, response) => { |
| 208 | const allowKeysExposure = getConfigValue('allowKeysExposure', false); | 208 | const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean'); |
| 209 | const key = request.body.key; | 209 | const key = request.body.key; |
| 210 | 210 | ||
| 211 | if (!allowKeysExposure && !EXPORTABLE_KEYS.includes(key)) { | 211 | if (!allowKeysExposure && !EXPORTABLE_KEYS.includes(key)) { |
| @@ -11,9 +11,9 @@ import { jsonParser } from '../express-common.js'; | |||
| 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; | 11 | import { getAllUserHandles, getUserDirectories } from '../users.js'; |
| 12 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | 12 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; |
| 13 | 13 | ||
| 14 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); | 14 | const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true, 'boolean'); |
| 15 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); | 15 | const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true, 'boolean'); |
| 16 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); | 16 | const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean'); |
| 17 | 17 | ||
| 18 | // 10 minutes | 18 | // 10 minutes |
| 19 | const AUTOSAVE_INTERVAL = 10 * 60 * 1000; | 19 | const AUTOSAVE_INTERVAL = 10 * 60 * 1000; |
| @@ -12,12 +12,15 @@ import { getAllUserHandles, getUserDirectories } from '../users.js'; | |||
| 12 | import { getConfigValue } from '../util.js'; | 12 | import { getConfigValue } from '../util.js'; |
| 13 | import { jsonParser } from '../express-common.js'; | 13 | import { jsonParser } from '../express-common.js'; |
| 14 | 14 | ||
| 15 | const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true); | 15 | const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean'); |
| 16 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95)))); | 16 | const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number')))); |
| 17 | const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; | 17 | const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png'; |
| 18 | 18 | ||
| 19 | /** @type {Record<string, number[]>} */ | 19 | /** @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 | }; | ||
| 21 | 24 | ||
| 22 | /** | 25 | /** |
| 23 | * Gets a path to thumbnail folder based on the type. | 26 | * Gets a path to thumbnail folder based on the type. |
| @@ -56,7 +56,7 @@ export const TEXT_COMPLETION_MODELS = [ | |||
| 56 | ]; | 56 | ]; |
| 57 | 57 | ||
| 58 | const CHARS_PER_TOKEN = 3.35; | 58 | const CHARS_PER_TOKEN = 3.35; |
| 59 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true); | 59 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); |
| 60 | 60 | ||
| 61 | /** | 61 | /** |
| 62 | * Gets a path to the tokenizer model. Downloads the model if it's a URL. | 62 | * Gets a path to the tokenizer model. Downloads the model if it's a URL. |
| @@ -7,8 +7,8 @@ import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-co | |||
| 7 | import { color, Cache, getConfigValue } from '../util.js'; | 7 | import { color, Cache, getConfigValue } from '../util.js'; |
| 8 | import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; | 8 | import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; |
| 9 | 9 | ||
| 10 | const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false); | 10 | const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false, 'boolean'); |
| 11 | const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false); | 11 | const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean'); |
| 12 | const MFA_CACHE = new Cache(5 * 60 * 1000); | 12 | const MFA_CACHE = new Cache(5 * 60 * 1000); |
| 13 | 13 | ||
| 14 | const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request); | 14 | const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request); |
| @@ -3,7 +3,7 @@ import fs from 'node:fs'; | |||
| 3 | import { getRealIpFromHeader } from '../express-common.js'; | 3 | import { getRealIpFromHeader } from '../express-common.js'; |
| 4 | import { color, getConfigValue } from '../util.js'; | 4 | import { color, getConfigValue } from '../util.js'; |
| 5 | 5 | ||
| 6 | const enableAccessLog = getConfigValue('logging.enableAccessLog', true); | 6 | const enableAccessLog = getConfigValue('logging.enableAccessLog', true, 'boolean'); |
| 7 | 7 | ||
| 8 | const knownIPs = new Set(); | 8 | const knownIPs = new Set(); |
| 9 | 9 | ||
| @@ -5,10 +5,10 @@ | |||
| 5 | import { Buffer } from 'node:buffer'; | 5 | import { Buffer } from 'node:buffer'; |
| 6 | import storage from 'node-persist'; | 6 | import storage from 'node-persist'; |
| 7 | import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; | 7 | import { getAllUserHandles, toKey, getPasswordHash } from '../users.js'; |
| 8 | import { getConfig, getConfigValue, safeReadFileSync } from '../util.js'; | 8 | import { getConfigValue, safeReadFileSync } from '../util.js'; |
| 9 | 9 | ||
| 10 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); | 10 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); |
| 11 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); | 11 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); |
| 12 | 12 | ||
| 13 | const basicAuthMiddleware = async function (request, response, callback) { | 13 | const basicAuthMiddleware = async function (request, response, callback) { |
| 14 | const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? ''; | 14 | const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? ''; |
| @@ -17,7 +17,8 @@ const basicAuthMiddleware = async function (request, response, callback) { | |||
| 17 | return res.status(401).send(unauthorizedWebpage); | 17 | return res.status(401).send(unauthorizedWebpage); |
| 18 | }; | 18 | }; |
| 19 | 19 | ||
| 20 | const config = getConfig(); | 20 | const basicAuthUserName = getConfigValue('basicAuthUser.username'); |
| 21 | const basicAuthUserPassword = getConfigValue('basicAuthUser.password'); | ||
| 21 | const authHeader = request.headers.authorization; | 22 | const authHeader = request.headers.authorization; |
| 22 | 23 | ||
| 23 | if (!authHeader) { | 24 | if (!authHeader) { |
| @@ -35,7 +36,7 @@ const basicAuthMiddleware = async function (request, response, callback) { | |||
| 35 | .toString('utf8') | 36 | .toString('utf8') |
| 36 | .split(':'); | 37 | .split(':'); |
| 37 | 38 | ||
| 38 | if (!usePerUserAuth && username === config.basicAuthUser.username && password === config.basicAuthUser.password) { | 39 | if (!usePerUserAuth && username === basicAuthUserName && password === basicAuthUserPassword) { |
| 39 | return callback(); | 40 | return callback(); |
| 40 | } else if (usePerUserAuth) { | 41 | } else if (usePerUserAuth) { |
| 41 | const userHandles = await getAllUserHandles(); | 42 | const userHandles = await getAllUserHandles(); |
| @@ -8,7 +8,7 @@ import { getIpFromRequest } from '../express-common.js'; | |||
| 8 | import { color, getConfigValue, safeReadFileSync } from '../util.js'; | 8 | import { color, getConfigValue, safeReadFileSync } from '../util.js'; |
| 9 | 9 | ||
| 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); | 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); | 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean'); |
| 12 | let whitelist = getConfigValue('whitelist', []); | 12 | let whitelist = getConfigValue('whitelist', []); |
| 13 | 13 | ||
| 14 | if (fs.existsSync(whitelistPath)) { | 14 | if (fs.existsSync(whitelistPath)) { |
| @@ -7,8 +7,8 @@ import { default as git, CheckRepoActions } from 'simple-git'; | |||
| 7 | import { sync as commandExistsSync } from 'command-exists'; | 7 | import { sync as commandExistsSync } from 'command-exists'; |
| 8 | import { getConfigValue, color } from './util.js'; | 8 | import { getConfigValue, color } from './util.js'; |
| 9 | 9 | ||
| 10 | const enableServerPlugins = !!getConfigValue('enableServerPlugins', false); | 10 | const enableServerPlugins = !!getConfigValue('enableServerPlugins', false, 'boolean'); |
| 11 | const enableServerPluginsAutoUpdate = !!getConfigValue('enableServerPluginsAutoUpdate', true); | 11 | const enableServerPluginsAutoUpdate = !!getConfigValue('enableServerPluginsAutoUpdate', true, 'boolean'); |
| 12 | 12 | ||
| 13 | /** | 13 | /** |
| 14 | * Map of loaded plugins. | 14 | * Map of loaded plugins. |
| @@ -572,7 +572,7 @@ export function convertMistralMessages(messages, names) { | |||
| 572 | } | 572 | } |
| 573 | 573 | ||
| 574 | // Make the last assistant message a prefill | 574 | // Make the last assistant message a prefill |
| 575 | const prefixEnabled = getConfigValue('mistral.enablePrefix', false); | 575 | const prefixEnabled = getConfigValue('mistral.enablePrefix', false, 'boolean'); |
| 576 | const lastMsg = messages[messages.length - 1]; | 576 | const lastMsg = messages[messages.length - 1]; |
| 577 | if (prefixEnabled && messages.length > 0 && lastMsg?.role === 'assistant') { | 577 | if (prefixEnabled && messages.length > 0 && lastMsg?.role === 'assistant') { |
| 578 | lastMsg.prefix = true; | 578 | lastMsg.prefix = true; |
| @@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') { | |||
| 132 | 132 | ||
| 133 | const cacheDir = path.join(globalThis.DATA_ROOT, '_cache'); | 133 | const cacheDir = path.join(globalThis.DATA_ROOT, '_cache'); |
| 134 | const model = forceModel || getModelForTask(task); | 134 | const model = forceModel || getModelForTask(task); |
| 135 | const localOnly = !getConfigValue('extensions.models.autoDownload', true); | 135 | const localOnly = !getConfigValue('extensions.models.autoDownload', true, 'boolean'); |
| 136 | console.log('Initializing transformers.js pipeline for task', task, 'with model', model); | 136 | console.log('Initializing transformers.js pipeline for task', task, 'with model', model); |
| 137 | const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly }); | 137 | const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly }); |
| 138 | tasks[task].pipeline = instance; | 138 | tasks[task].pipeline = instance; |
| @@ -15,15 +15,15 @@ import _ from 'lodash'; | |||
| 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 15 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 16 | 16 | ||
| 17 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; | 17 | import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE } from './constants.js'; |
| 18 | import { getConfigValue, color, delay, setConfigValue, generateTimestamp } from './util.js'; | 18 | import { getConfigValue, color, delay, generateTimestamp } from './util.js'; |
| 19 | import { readSecret, writeSecret } from './endpoints/secrets.js'; | 19 | import { readSecret, writeSecret } from './endpoints/secrets.js'; |
| 20 | import { getContentOfType } from './endpoints/content-manager.js'; | 20 | import { getContentOfType } from './endpoints/content-manager.js'; |
| 21 | 21 | ||
| 22 | export const KEY_PREFIX = 'user:'; | 22 | export const KEY_PREFIX = 'user:'; |
| 23 | const AVATAR_PREFIX = 'avatar:'; | 23 | const AVATAR_PREFIX = 'avatar:'; |
| 24 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); | 24 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); |
| 25 | const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false); | 25 | const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false, 'boolean'); |
| 26 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); | 26 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); |
| 27 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); | 27 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); |
| 28 | 28 | ||
| 29 | /** | 29 | /** |
| @@ -32,9 +32,13 @@ const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); | |||
| 32 | */ | 32 | */ |
| 33 | const DIRECTORIES_CACHE = new Map(); | 33 | const DIRECTORIES_CACHE = new Map(); |
| 34 | const PUBLIC_USER_AVATAR = '/img/default-user.png'; | 34 | const PUBLIC_USER_AVATAR = '/img/default-user.png'; |
| 35 | const COOKIE_SECRET_PATH = 'cookie-secret.txt'; | ||
| 35 | 36 | ||
| 36 | const STORAGE_KEYS = { | 37 | const STORAGE_KEYS = { |
| 37 | csrfSecret: 'csrfSecret', | 38 | csrfSecret: 'csrfSecret', |
| 39 | /** | ||
| 40 | * @deprecated Read from COOKIE_SECRET_PATH in DATA_ROOT instead. | ||
| 41 | */ | ||
| 38 | cookieSecret: 'cookieSecret', | 42 | cookieSecret: 'cookieSecret', |
| 39 | }; | 43 | }; |
| 40 | 44 | ||
| @@ -412,11 +416,10 @@ export function toAvatarKey(handle) { | |||
| 412 | * @returns {Promise<void>} | 416 | * @returns {Promise<void>} |
| 413 | */ | 417 | */ |
| 414 | export async function initUserStorage(dataRoot) { | 418 | export 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)); | ||
| 417 | console.log(); | 420 | console.log(); |
| 418 | await storage.init({ | 421 | await storage.init({ |
| 419 | dir: path.join(globalThis.DATA_ROOT, '_storage'), | 422 | dir: path.join(dataRoot, '_storage'), |
| 420 | ttl: false, // Never expire | 423 | ttl: false, // Never expire |
| 421 | }); | 424 | }); |
| 422 | 425 | ||
| @@ -430,17 +433,29 @@ export async function initUserStorage(dataRoot) { | |||
| 430 | 433 | ||
| 431 | /** | 434 | /** |
| 432 | * Get the cookie secret from the config. If it doesn't exist, generate a new one. | 435 | * 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 | ||
| 433 | * @returns {string} The cookie secret | 437 | * @returns {string} The cookie secret |
| 434 | */ | 438 | */ |
| 435 | export function getCookieSecret() { | 439 | export function getCookieSecret(dataRoot) { |
| 436 | let secret = getConfigValue(STORAGE_KEYS.cookieSecret); | 440 | const cookieSecretPath = path.join(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 | } | ||
| 437 | 448 | ||
| 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; | ||
| 442 | } | 454 | } |
| 443 | 455 | ||
| 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' }); | ||
| 444 | return secret; | 459 | return secret; |
| 445 | } | 460 | } |
| 446 | 461 | ||
| @@ -9,7 +9,6 @@ import { promises as dnsPromise } from 'node:dns'; | |||
| 9 | 9 | ||
| 10 | import yaml from 'yaml'; | 10 | import yaml from 'yaml'; |
| 11 | import { sync as commandExistsSync } from 'command-exists'; | 11 | import { sync as commandExistsSync } from 'command-exists'; |
| 12 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | ||
| 13 | import _ from 'lodash'; | 12 | import _ from 'lodash'; |
| 14 | import yauzl from 'yauzl'; | 13 | import yauzl from 'yauzl'; |
| 15 | import mime from 'mime-types'; | 14 | import mime from 'mime-types'; |
| @@ -22,6 +21,14 @@ import { LOG_LEVELS } from './constants.js'; | |||
| 22 | let CACHED_CONFIG = null; | 21 | let CACHED_CONFIG = null; |
| 23 | 22 | ||
| 24 | /** | 23 | /** |
| 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 | /** | ||
| 25 | * Returns the config object from the config.yaml file. | 32 | * Returns the config object from the config.yaml file. |
| 26 | * @returns {object} Config object | 33 | * @returns {object} Config object |
| 27 | */ | 34 | */ |
| @@ -51,24 +58,40 @@ export function getConfig() { | |||
| 51 | * Returns the value for the given key from the config object. | 58 | * Returns the value for the given key from the config object. |
| 52 | * @param {string} key - Key to get from the config object | 59 | * @param {string} key - Key to get from the config object |
| 53 | * @param {any} defaultValue - Default value to return if the key is not found | 60 | * @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 | ||
| 54 | * @returns {any} Value for the given key | 62 | * @returns {any} Value for the given key |
| 55 | */ | 63 | */ |
| 56 | export function getConfigValue(key, defaultValue = null) { | 64 | export function getConfigValue(key, defaultValue = null, typeConverter = null) { |
| 65 | function _getValue() { | ||
| 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 | } | ||
| 57 | const config = getConfig(); | 72 | const config = getConfig(); |
| 58 | return _.get(config, key, defaultValue); | 73 | return _.get(config, key, defaultValue); |
| 59 | } | 74 | } |
| 60 | 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 | } | ||
| 85 | } | ||
| 86 | |||
| 61 | /** | 87 | /** |
| 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. |
| 63 | * @param {string} key Key to set | 89 | * @param {any} _key Unused |
| 64 | * @param {any} value Value to set | 90 | * @param {any} _value Unused |
| 91 | * @deprecated Configs are read-only. Use environment variables instead. | ||
| 65 | */ | 92 | */ |
| 66 | export function setConfigValue(key, value) { | 93 | export function setConfigValue(_key, _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)); | ||
| 72 | } | 95 | } |
| 73 | 96 | ||
| 74 | /** | 97 | /** |
| @@ -394,7 +417,7 @@ export function generateTimestamp() { | |||
| 394 | * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `backups.common.numberOfBackups` config value. | 417 | * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `backups.common.numberOfBackups` config value. |
| 395 | */ | 418 | */ |
| 396 | export function removeOldBackups(directory, prefix, limit = null) { | 419 | export function removeOldBackups(directory, prefix, limit = null) { |
| 397 | const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50)); | 420 | const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50, 'number')); |
| 398 | 421 | ||
| 399 | let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); | 422 | let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); |
| 400 | if (files.length > MAX_BACKUPS) { | 423 | if (files.length > MAX_BACKUPS) { |
| @@ -747,6 +770,27 @@ export async function canResolve(name, useIPv6 = true, useIPv4 = true) { | |||
| 747 | } | 770 | } |
| 748 | } | 771 | } |
| 749 | 772 | ||
| 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 | } | ||
| 750 | 794 | ||
| 751 | /** | 795 | /** |
| 752 | * converts string to boolean accepts 'true' or 'false' else it returns the string put in | 796 | * 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) { | |||
| 754 | * @returns {boolean|string|null} boolean else original input string or null if input is | 798 | * @returns {boolean|string|null} boolean else original input string or null if input is |
| 755 | */ | 799 | */ |
| 756 | export function stringToBool(str) { | 800 | export function stringToBool(str) { |
| 757 | if (str === 'true') return true; | 801 | if (String(str).trim().toLowerCase() === 'true') return true; |
| 758 | if (str === 'false') return false; | 802 | if (String(str).trim().toLowerCase() === 'false') return false; |
| 759 | return str; | 803 | return str; |
| 760 | } | 804 | } |
| 761 | 805 | ||
| @@ -763,7 +807,7 @@ export function stringToBool(str) { | |||
| 763 | * Setup the minimum log level | 807 | * Setup the minimum log level |
| 764 | */ | 808 | */ |
| 765 | export function setupLogLevel() { | 809 | export function setupLogLevel() { |
| 766 | const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG); | 810 | const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG, 'number'); |
| 767 | 811 | ||
| 768 | globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {}; | 812 | globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {}; |
| 769 | globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {}; | 813 | globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {}; |