cangku/scripts/remote-access-common.mjs

81 lines
2.7 KiB
JavaScript

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',
'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('注册服务返回的数据库名无效');
}
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 };
}