| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { readFileSync, existsSync } from 'fs'; |
| import { scryptSync, randomBytes, timingSafeEqual } from 'crypto'; |
| import { writeJsonAtomic } from './fs-atomic.js'; |
| import { resolve } from 'path'; |
| import { config, log } from './config.js'; |
|
|
| const FILE = resolve(config.dataDir, 'runtime-config.json'); |
|
|
| const DEFAULTS = { |
| experimental: { |
| |
| |
| |
| cascadeConversationReuse: true, |
| |
| |
| |
| preflightRateLimit: false, |
| |
| |
| |
| |
| |
| droughtRestrictPremium: true, |
| |
| |
| |
| |
| |
| |
| |
| autoUpdateQuietWindow: false, |
| }, |
| |
| |
| |
| autoUpdateQuietWindow: { |
| windowMinutes: 5, |
| thresholdRequests: 5, |
| cooldownHours: 24, |
| coldStartGraceMs: 600000, |
| }, |
| |
| |
| systemPrompts: { |
| toolReinforcement: 'The functions listed above are available and callable. When the user\'s request can be answered by calling a function, emit a <tool_call> block as described. Use this exact format: <tool_call>{"name":"...","arguments":{...}}</tool_call>', |
| communicationWithTools: 'You are accessed via API. When asked about your identity, describe your actual underlying model name and provider accurately. STRICTLY respond in the exact same language the user used in their latest message (Chinese β Chinese, English β English, Japanese β Japanese; never switch mid-conversation). Use the functions above when relevant.', |
| communicationNoTools: 'You are accessed via API. When asked about your identity, describe your actual underlying model name and provider accurately. Answer directly. STRICTLY respond in the exact same language the user used in their latest message (Chinese β Chinese, English β English, Japanese β Japanese; never switch mid-conversation).', |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| credentials: { |
| apiKey: '', |
| dashboardPasswordHash: '', |
| }, |
| }; |
|
|
| const SYSTEM_PROMPT_KEYS = new Set(Object.keys(DEFAULTS.systemPrompts)); |
|
|
| function deepMerge(base, override) { |
| if (!override || typeof override !== 'object') return base; |
| const out = { ...base }; |
| for (const [k, v] of Object.entries(override)) { |
| |
| |
| |
| if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue; |
| if (v && typeof v === 'object' && !Array.isArray(v)) { |
| out[k] = deepMerge(base[k] || {}, v); |
| } else { |
| out[k] = v; |
| } |
| } |
| return out; |
| } |
|
|
| let _state = structuredClone(DEFAULTS); |
|
|
| function load() { |
| if (!existsSync(FILE)) return; |
| try { |
| const raw = JSON.parse(readFileSync(FILE, 'utf-8')); |
| _state = deepMerge(DEFAULTS, raw); |
| } catch (e) { |
| log.warn(`runtime-config: failed to load ${FILE}: ${e.message}`); |
| } |
| } |
|
|
| function persist() { |
| try { |
| writeJsonAtomic(FILE, _state); |
| } catch (e) { |
| log.warn(`runtime-config: failed to persist: ${e.message}`); |
| } |
| } |
|
|
| load(); |
|
|
| export function getRuntimeConfig() { |
| return structuredClone(_state); |
| } |
|
|
| export function getExperimental() { |
| return { ...(_state.experimental || {}) }; |
| } |
|
|
| export function isExperimentalEnabled(key) { |
| return !!_state.experimental?.[key]; |
| } |
|
|
| export function setExperimental(patch) { |
| if (!patch || typeof patch !== 'object') return getExperimental(); |
| _state.experimental = { ...(_state.experimental || {}), ...patch }; |
| |
| |
| for (const k of Object.keys(_state.experimental)) { |
| _state.experimental[k] = !!_state.experimental[k]; |
| } |
| persist(); |
| return getExperimental(); |
| } |
|
|
| export function getSystemPrompts() { |
| const out = { ...DEFAULTS.systemPrompts }; |
| for (const key of SYSTEM_PROMPT_KEYS) { |
| if (typeof _state.systemPrompts?.[key] === 'string') { |
| out[key] = _state.systemPrompts[key]; |
| } |
| } |
| return out; |
| } |
|
|
| export function setSystemPrompts(patch) { |
| if (!patch || typeof patch !== 'object') return getSystemPrompts(); |
| const current = _state.systemPrompts || {}; |
| for (const [k, v] of Object.entries(patch)) { |
| if (!SYSTEM_PROMPT_KEYS.has(k)) continue; |
| if (typeof v !== 'string') continue; |
| current[k] = v.trim(); |
| } |
| _state.systemPrompts = current; |
| persist(); |
| return getSystemPrompts(); |
| } |
|
|
| export function resetSystemPrompt(key) { |
| if (key) { |
| if (_state.systemPrompts && SYSTEM_PROMPT_KEYS.has(key)) delete _state.systemPrompts[key]; |
| } else { |
| _state.systemPrompts = {}; |
| } |
| persist(); |
| return getSystemPrompts(); |
| } |
|
|
| |
|
|
| const SCRYPT_N = 2 ** 14; |
| const SCRYPT_R = 8; |
| const SCRYPT_P = 1; |
| const SCRYPT_KEYLEN = 32; |
|
|
| |
| |
| |
| |
| |
| export function hashPassword(plain) { |
| const s = String(plain ?? ''); |
| if (!s) return ''; |
| const salt = randomBytes(16); |
| const hash = scryptSync(s, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }); |
| return `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${salt.toString('base64')}$${hash.toString('base64')}`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function verifyPassword(plain, stored) { |
| if (typeof stored !== 'string' || !stored) return false; |
| const sPlain = String(plain ?? ''); |
| if (!stored.startsWith('scrypt$')) { |
| |
| |
| |
| if (!sPlain) return false; |
| const a = Buffer.from(sPlain, 'utf8'); |
| const b = Buffer.from(stored, 'utf8'); |
| if (a.length !== b.length) { |
| |
| |
| try { timingSafeEqual(Buffer.alloc(b.length), Buffer.alloc(b.length)); } catch {} |
| return false; |
| } |
| return timingSafeEqual(a, b); |
| } |
| const parts = stored.split('$'); |
| if (parts.length !== 6) return false; |
| const N = parseInt(parts[1], 10); |
| const r = parseInt(parts[2], 10); |
| const p = parseInt(parts[3], 10); |
| if (!Number.isFinite(N) || !Number.isFinite(r) || !Number.isFinite(p)) return false; |
| let salt, expected; |
| try { |
| salt = Buffer.from(parts[4], 'base64'); |
| expected = Buffer.from(parts[5], 'base64'); |
| } catch { return false; } |
| if (!salt.length || !expected.length) return false; |
| const actual = scryptSync(sPlain, salt, expected.length, { N, r, p }); |
| return actual.length === expected.length && timingSafeEqual(actual, expected); |
| } |
|
|
| export function getCredentials() { |
| return { |
| apiKey: _state.credentials?.apiKey || '', |
| dashboardPasswordHash: _state.credentials?.dashboardPasswordHash || '', |
| }; |
| } |
|
|
| |
| |
| |
| |
| export function setRuntimeApiKey(plain) { |
| const v = typeof plain === 'string' ? plain.trim() : ''; |
| if (!_state.credentials) _state.credentials = {}; |
| _state.credentials.apiKey = v; |
| persist(); |
| return getCredentials(); |
| } |
|
|
| |
| |
| |
| |
| export function setRuntimeDashboardPassword(plain) { |
| const v = typeof plain === 'string' ? plain : ''; |
| if (!_state.credentials) _state.credentials = {}; |
| _state.credentials.dashboardPasswordHash = v ? hashPassword(v) : ''; |
| persist(); |
| return getCredentials(); |
| } |
|
|
| |
| |
| |
| |
| export function getEffectiveApiKey() { |
| const runtime = _state.credentials?.apiKey || ''; |
| return runtime || config.apiKey || ''; |
| } |
|
|
| |
| |
| |
| |
| |
| export function getEffectiveDashboardPasswordStored() { |
| const runtime = _state.credentials?.dashboardPasswordHash || ''; |
| return runtime || config.dashboardPassword || ''; |
| } |
|
|
| |
| |
| |
| import('./auth.js').then(m => { |
| if (typeof m.setApiKeyResolver === 'function') m.setApiKeyResolver(getEffectiveApiKey); |
| |
| |
| if (typeof m.setDroughtRestrictResolver === 'function') { |
| m.setDroughtRestrictResolver(() => isExperimentalEnabled('droughtRestrictPremium')); |
| } |
| }).catch(() => { }); |
|
|
|
|