import fs from 'node:fs'; import path from 'node:path'; export const REQUIRED_ENROLLMENT_FIELDS = Object.freeze([ 'installation_id', 'ssh.host', 'ssh.port', 'ssh.user', 'ssh.certificate', 'ssh.host_key', 'database.name', 'database.username', 'database.password', 'redis.host', 'redis.port', 'redis.username', 'redis.password', 'redis.db', 'redis.tls', 'redis.tls_server_name', 'application_admin.username', 'application_admin.password', ]); function readNested(value, field) { return field.split('.').reduce((current, part) => current?.[part], value); } export function validateEnrollmentResponse(value) { if (!value || typeof value !== 'object') { throw new Error('注册服务返回了无效数据'); } for (const field of REQUIRED_ENROLLMENT_FIELDS) { if (readNested(value, field) === undefined || readNested(value, field) === '') { throw new Error(`注册服务缺少字段:${field}`); } } if (!String(value.ssh.certificate).startsWith('ssh-ed25519-cert-v01@openssh.com ')) { throw new Error('注册服务返回的 SSH 证书格式无效'); } if (!/^ssh-ed25519\s+[A-Za-z0-9+/=]+(?:\s.*)?$/u.test(String(value.ssh.host_key))) { throw new Error('注册服务返回的 SSH 主机公钥格式无效'); } if (!/^[A-Za-z0-9_]+$/u.test(String(value.database.name))) { throw new Error('注册服务返回的数据库名无效'); } for (const [field, minimum] of [['redis.port', 1]]) { const port = Number(readNested(value, field)); if (!Number.isInteger(port) || port < minimum || port > 65535) { throw new Error(`注册服务返回的端口无效:${field}`); } } if (!String(value.redis.host).trim() || !String(value.redis.tls_server_name).trim()) { throw new Error('注册服务返回的 Redis TLS 地址无效'); } if (value.redis.tls !== true) { throw new Error('注册服务未启用 Redis TLS'); } const redisDb = Number(value.redis.db); if (!Number.isInteger(redisDb) || redisDb < 0 || redisDb > 15) { throw new Error('注册服务返回的 Redis 数据库编号无效'); } return value; } export function serializeEnvValue(value) { const text = String(value ?? ''); if (!text) return ''; if (/[^A-Za-z0-9_./:@+-]/u.test(text) || /[#$'"\\]/u.test(text)) { return JSON.stringify(text); } return text; } export function mergeEnv(content, updates) { const lines = String(content || '').split(/\r?\n/u); const handled = new Set(); const output = lines.map((line) => { const match = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/u); if (!match || !(match[1] in updates)) return line; handled.add(match[1]); return `${match[1]}=${serializeEnvValue(updates[match[1]])}`; }); for (const [key, value] of Object.entries(updates)) { if (!handled.has(key)) output.push(`${key}=${serializeEnvValue(value)}`); } while (output.length > 0 && output.at(-1) === '') output.pop(); return `${output.join('\n')}\n`; } export function getKnownHostsLine(host, port, hostKey) { const target = Number(port) === 22 ? host : `[${host}]:${port}`; return `${target} ${String(hostKey).trim()}\n`; } export function loadConnectionConfig(rootDirectory) { const configPath = path.join(rootDirectory, '.remote-access', 'connection.json'); const value = JSON.parse(fs.readFileSync(configPath, 'utf8')); if (!value?.installationId || !value?.ssh?.host || !value?.localDatabasePort) { throw new Error('本地远程访问配置不完整,请重新执行 npm run remote:setup'); } return { configPath, value }; }