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