'use strict'; const fs = require('fs'); const os = require('os'); const path = require('path'); const database = require('../config/database'); const SECRET_TABLE = 'system_secrets'; const SECRET_STORE_VERSION = 1; let schemaReady = false; let schemaPromise = null; let backupWriteQueue = Promise.resolve(); function normalizeSecretKey(value) { const key = String(value || '').trim(); if (!/^[a-z0-9][a-z0-9_.-]{2,119}$/i.test(key)) { throw new Error('持久密钥名称格式无效'); } return key; } function normalizeSecretValue(value) { return String(value || '').trim().slice(0, 10000); } function firstSecret(values = []) { for (const value of values) { const normalized = normalizeSecretValue(value); if (normalized) return normalized; } return ''; } function getSecretBackupPath() { const configured = String(process.env.SHUJUKU3_SECRET_STORE_PATH || '').trim(); return configured || path.join(os.homedir(), '.config', 'shujuku3', 'secrets.json'); } function getLegacySecretBackupPath() { const configured = String(process.env.SHUJUKU3_LEGACY_SECRET_STORE_PATH || '').trim(); return configured || path.join(os.homedir(), '.config', 'shujuku3', 'secrets.json'); } function getSecretBackupCandidatePaths() { return [...new Set([getSecretBackupPath(), getLegacySecretBackupPath()])]; } function readSecretBackup(secretKey) { const key = normalizeSecretKey(secretKey); for (const filePath of getSecretBackupCandidatePaths()) { if (!fs.existsSync(filePath)) continue; try { const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')); const value = normalizeSecretValue(parsed?.secrets?.[key]); if (value) return value; } catch (_error) { // Continue to the legacy backup when the primary file is incomplete. } } return ''; } function writeSecretBackupNow(secretKey, secretValue) { const key = normalizeSecretKey(secretKey); const value = normalizeSecretValue(secretValue); if (!value) throw new Error('持久密钥不能为空'); const filePath = getSecretBackupPath(); const directory = path.dirname(filePath); fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); try { fs.chmodSync(directory, 0o700); } catch (_error) {} let current = {}; if (fs.existsSync(filePath)) { try { current = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; } catch (_error) {} } const payload = { version: SECRET_STORE_VERSION, updatedAt: new Date().toISOString(), secrets: { ...(current.secrets && typeof current.secrets === 'object' ? current.secrets : {}), [key]: value, }, }; const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; fs.writeFileSync(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }); fs.renameSync(temporaryPath, filePath); try { fs.chmodSync(filePath, 0o600); } catch (_error) {} } function writeSecretBackup(secretKey, secretValue) { const operation = backupWriteQueue.then(() => writeSecretBackupNow(secretKey, secretValue)); backupWriteQueue = operation.catch(() => {}); return operation; } async function ensureSecretSchema() { if (schemaReady) return; if (schemaPromise) return schemaPromise; schemaPromise = database.local.query( `CREATE TABLE IF NOT EXISTS ${SECRET_TABLE} ( secret_key VARCHAR(120) PRIMARY KEY COMMENT '密钥用途', secret_value TEXT NOT NULL COMMENT '仅服务端读取的密钥值', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统持久密钥'` ).then(() => { schemaReady = true; }).finally(() => { schemaPromise = null; }); return schemaPromise; } async function readDatabaseSecret(secretKey) { const key = normalizeSecretKey(secretKey); await ensureSecretSchema(); const rows = await database.local.query( `SELECT secret_value AS secretValue FROM ${SECRET_TABLE} WHERE secret_key = ? LIMIT 1`, [key] ); return normalizeSecretValue(rows[0]?.secretValue); } async function writeDatabaseSecret(secretKey, secretValue) { const key = normalizeSecretKey(secretKey); const value = normalizeSecretValue(secretValue); if (!value) throw new Error('持久密钥不能为空'); await ensureSecretSchema(); await database.local.query( `INSERT INTO ${SECRET_TABLE} (secret_key, secret_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE secret_value = VALUES(secret_value), updated_at = CURRENT_TIMESTAMP`, [key, value] ); return value; } async function backupSecretQuietly(secretKey, secretValue) { try { await writeSecretBackup(secretKey, secretValue); } catch (error) { console.warn(`持久密钥文件备份失败(${secretKey}):`, error.message); } } async function restoreDatabaseSecretQuietly(secretKey, secretValue) { try { await writeDatabaseSecret(secretKey, secretValue); } catch (error) { console.warn(`持久密钥数据库恢复失败(${secretKey}):`, error.message); } } async function getResilientSecret(secretKey, options = {}) { const key = normalizeSecretKey(secretKey); try { const databaseValue = await readDatabaseSecret(key); if (databaseValue) { await backupSecretQuietly(key, databaseValue); return { value: databaseValue, source: 'database' }; } } catch (error) { console.warn(`读取持久密钥数据库失败(${key}):`, error.message); } const legacyValue = firstSecret(options.legacyValues); if (legacyValue) { await restoreDatabaseSecretQuietly(key, legacyValue); await backupSecretQuietly(key, legacyValue); return { value: legacyValue, source: 'legacy-database' }; } const backupValue = readSecretBackup(key); if (backupValue) { await restoreDatabaseSecretQuietly(key, backupValue); await backupSecretQuietly(key, backupValue); return { value: backupValue, source: 'persistent-backup' }; } const fallbackValue = firstSecret(options.fallbackValues); if (fallbackValue) { await restoreDatabaseSecretQuietly(key, fallbackValue); await backupSecretQuietly(key, fallbackValue); return { value: fallbackValue, source: 'environment' }; } return { value: '', source: 'unconfigured' }; } async function saveResilientSecret(secretKey, secretValue) { const key = normalizeSecretKey(secretKey); const value = await writeDatabaseSecret(key, secretValue); await backupSecretQuietly(key, value); return { value, source: 'database' }; } module.exports = { SECRET_TABLE, ensureSecretSchema, getResilientSecret, getSecretBackupCandidatePaths, getSecretBackupPath, readSecretBackup, saveResilientSecret, writeSecretBackup, };