diff --git a/src/middleware/auth.js b/src/middleware/auth.js index c6287c4..b7fd4ad 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -78,8 +78,13 @@ async function authMiddleware(req, res, next) { return res.status(401).json({ error: 'Session expired due to inactivity', idleTimeout: true }); } - // Throttled last_activity update — at most every 5 min - if (idleMs > ACTIVITY_THROTTLE_MS) { + // Only state-changing HTTP methods count as "user activity" for + // sliding-idle purposes. GET requests are often automated (poll + // loops, dashboards, /api/auth/me heartbeats) and would otherwise + // keep a session alive forever, defeating the 24h idle policy. + var isWriteMethod = req.method === 'POST' || req.method === 'PUT' + || req.method === 'DELETE' || req.method === 'PATCH'; + if (isWriteMethod && idleMs > ACTIVITY_THROTTLE_MS) { db.run('UPDATE user_sessions SET last_activity = NOW() WHERE id = ?', [session.id]).catch(function() {}); // Sliding cookie refresh (web only) — extend browser-side lifetime too if (!mobile && req.cookies && req.cookies.ped_auth) { diff --git a/src/utils/ai.js b/src/utils/ai.js index 53b3ea1..e091d23 100644 --- a/src/utils/ai.js +++ b/src/utils/ai.js @@ -400,11 +400,35 @@ async function callLiteLLM(messages, model, temperature, maxTokens) { // ============================================================ async function callAI(messages, options) { options = options || {}; - var model = options.model || DEFAULT_MODEL; + var requestedModel = options.model; + var model = requestedModel || DEFAULT_MODEL; var temperature = options.temperature || 0.3; var maxTokens = options.maxTokens || 4000; var startTime = Date.now(); + // Server-side whitelist: reject any model the operator hasn't enabled. + // Prevents a client from passing e.g. model:"openai/o1" and draining + // the budget on a reasoning model outside the configured roster. + // Falls back silently to DEFAULT_MODEL when no model was provided. + if (requestedModel) { + try { + var db = require('../db/database'); + var { getAllowedModelIds } = require('./models'); + var allowed = await getAllowedModelIds(db); + if (allowed && allowed.size > 0 && !allowed.has(requestedModel)) { + var err = new Error('Model not permitted'); + err.code = 'model_not_permitted'; + err.model = requestedModel; + throw err; + } + } catch (e) { + if (e && e.code === 'model_not_permitted') throw e; + // DB lookup failure — fall through, let the provider reject if it + // doesn't recognize the model. Log for visibility. + console.warn('[callAI] Allow-list check skipped:', e && e.message); + } + } + try { var result; diff --git a/src/utils/models.js b/src/utils/models.js index 9ee5877..eb938b7 100644 --- a/src/utils/models.js +++ b/src/utils/models.js @@ -222,6 +222,45 @@ async function getAvailableModelsWithOverrides(db) { } } +// Whitelist of allowed model IDs, cached in memory. Refreshed periodically +// from the DB so admin changes propagate without restart. Prevents +// client-supplied model values (e.g. POST /api/hpi with model="openai/o1") +// from calling expensive models outside the configured roster. +var ALLOWED_MODELS_CACHE = null; +var ALLOWED_MODELS_FETCHED = 0; +var ALLOWED_MODELS_TTL_MS = 60 * 1000; + +async function getAllowedModelIds(db) { + var now = Date.now(); + if (ALLOWED_MODELS_CACHE && (now - ALLOWED_MODELS_FETCHED) < ALLOWED_MODELS_TTL_MS) { + return ALLOWED_MODELS_CACHE; + } + try { + var models = await getAvailableModelsWithOverrides(db); + var ids = new Set((models || []).map(function(m) { return m.id; })); + ALLOWED_MODELS_CACHE = ids; + ALLOWED_MODELS_FETCHED = now; + return ids; + } catch (e) { + // Fall back to static list if DB fetch fails + var staticIds = new Set(getAvailableModels().map(function(m) { return m.id; })); + return staticIds; + } +} + +// Synchronous check against the static baseline (no DB access). Used when +// the async-aware helper is inconvenient. For full enforcement use +// getAllowedModelIds + Set.has() in the route before invoking callAI. +function isStaticAllowedModel(id) { + if (!id) return false; + return getAvailableModels().some(function(m) { return m.id === id; }); +} + +function invalidateAllowedModelsCache() { + ALLOWED_MODELS_CACHE = null; + ALLOWED_MODELS_FETCHED = 0; +} + module.exports = { AVAILABLE_MODELS, DEFAULT_MODEL, @@ -233,6 +272,9 @@ module.exports = { LITELLM_MODELS, getAvailableModels, getAvailableModelsWithOverrides, + getAllowedModelIds, + isStaticAllowedModel, + invalidateAllowedModelsCache, getDefaultModel, getFallbackModel, getBedrockModelId,