const fs = require("fs"); const path = require("path"); const metadata = require("../tool_info.json"); function defaults() { return Object.fromEntries( Object.entries(metadata.settings_schema).map(([key, field]) => [key, structuredClone(field.default)]) ); } function readSettings(dataDir) { const fallback = defaults(); try { const stored = JSON.parse(fs.readFileSync(settingsPath(dataDir), "utf8")); return normalizeSettings({ ...fallback, ...stored }); } catch { return normalizeSettings(fallback); } } function writeSettings(dataDir, value) { const normalized = normalizeSettings({ ...defaults(), ...value }); fs.mkdirSync(dataDir, { recursive: true }); const file = settingsPath(dataDir); const temporary = `${file}.${process.pid}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 }); try { fs.chmodSync(temporary, 0o600); } catch {} fs.renameSync(temporary, file); return normalized; } function normalizeSettings(value) { const mode = value.policy_mode === "blacklist" ? "blacklist" : "whitelist"; const adapter = value.provider_adapter === "generic_json" ? "generic_json" : "searxng_json"; const safeSearch = ["off", "moderate", "strict"].includes(value.safe_search) ? value.safe_search : "strict"; return { enabled: value.enabled === true, policy_mode: mode, url_rules: stringList(value.url_rules, 200), max_results: integer(value.max_results, 1, 10, 5), search_timeout_ms: integer(value.search_timeout_ms, 1000, 30000, 8000), cache_ttl_seconds: integer(value.cache_ttl_seconds, 0, 3600, 300), safe_search: safeSearch, allowed_origins: stringList(value.allowed_origins, 6) .filter((origin) => ["webui", "discord", "twitch", "youtube", "kick", "other"].includes(origin)), webui_output_chars: integer(value.webui_output_chars, 300, 12000, 4000), discord_output_chars: integer(value.discord_output_chars, 200, 4000, 1200), twitch_output_chars: integer(value.twitch_output_chars, 120, 1000, 350), youtube_output_chars: integer(value.youtube_output_chars, 120, 1500, 500), kick_output_chars: integer(value.kick_output_chars, 120, 1000, 350), other_output_chars: integer(value.other_output_chars, 120, 2000, 500), provider_adapter: adapter, provider_endpoint: String(value.provider_endpoint || "").trim(), provider_api_key: String(value.provider_api_key || "").trim(), provider_api_key_header: value.provider_api_key_header === "Authorization" ? "Authorization" : "X-API-Key", provider_api_key_prefix: String(value.provider_api_key_prefix || "").trim().slice(0, 32), provider_query_parameter: /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(String(value.provider_query_parameter || "")) ? String(value.provider_query_parameter) : "q", show_source_links: value.show_source_links !== false, allow_full_page_fetch: value.allow_full_page_fetch === true, requests_per_minute: integer(value.requests_per_minute, 1, 60, 6) }; } function settingsPath(dataDir) { return path.join(dataDir, "settings.json"); } function integer(value, minimum, maximum, fallback) { const number = Number.parseInt(value, 10); return Number.isFinite(number) ? Math.max(minimum, Math.min(maximum, number)) : fallback; } function stringList(value, limit) { const rows = Array.isArray(value) ? value : String(value || "").split(/\r?\n|,/); return [...new Set(rows.map((entry) => String(entry).trim()).filter(Boolean))].slice(0, limit); } module.exports = { defaults, normalizeSettings, readSettings, settingsPath, writeSettings };