/** * hal/config.mjs — HAL 실행 설정. * * 배포 때 정해지는 값(포트·데이터 디렉터리)은 환경 변수로, 운영 중 바꾸는 값(공개 주소·포털 주소·허용 네트워크·메트릭 보존 일수)은 * data/config.json 에 둔다. 비밀값(서비스 키·Agent 비밀)은 설정이 아니라 보관소(lib/vault)에만 둔다. */ import { readFileSync, existsSync, writeFileSync, mkdirSync, renameSync } from 'node:fs'; import { join } from 'node:path'; export function loadConfig() { const dataDir = process.env.ARIWEB_HAL_DATA || process.env.DATA_DIR || join(process.cwd(), 'data'); mkdirSync(dataDir, { recursive: true, mode: 0o700 }); const file = join(dataDir, 'config.json'); let values = {}; try { values = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')).values || {} : {}; } catch (_) { values = {}; } const cfg = { dataDir, file, port: Number(process.env.PORT || 8900), host: process.env.HOST || '127.0.0.1', /** HAL 공개 주소(Agent 가 접속하는 주소) */ publicUrl: values.publicUrl || process.env.HAL_PUBLIC_URL || 'https://hal.ariweb.net', /** 포털 내부 주소(작업 이벤트를 미는 곳) */ portalUrl: values.portalUrl || process.env.PORTAL_URL || 'http://127.0.0.1:8910', /** 운영자 콘솔 접근 허용 네트워크(CIDR 목록, §25). 비어 있으면 접근권(grant) 쿠키만 허용 */ opsAllowCidrs: values.opsAllowCidrs || (process.env.OPS_ALLOW_CIDRS ? process.env.OPS_ALLOW_CIDRS.split(',').map((s) => s.trim()).filter(Boolean) : ['127.0.0.1/32', '::1/128', '10.0.0.0/8', '192.168.0.0/16']), /** 메트릭 보존 일수(최소 90, §28.1) */ metricsRetentionDays: Math.max(90, Number(values.metricsRetentionDays || 90)), /** heartbeat 가 이만큼 없으면 서버를 offline 으로 본다 */ offlineAfterMs: Number(values.offlineAfterMs || 90 * 1000), /** 전송(transfer) 파일 보관 시간 */ transferTtlMs: Number(values.transferTtlMs || 24 * 3600 * 1000), /** 긴급 자격 최대 유효 시간(분) */ emergencyTtlMinutes: Math.min(60, Number(values.emergencyTtlMinutes || 30)), values, /** 설정 저장(운영자 화면·CLI). 비밀값은 거부한다. */ set(patch, by = 'ops') { for (const k of Object.keys(patch)) if (/key|secret|token|pass/i.test(k)) throw new Error(`비밀값(${k})은 설정 파일에 둘 수 없습니다 — 보관소를 쓰세요`); Object.assign(values, patch); const tmp = file + '.tmp'; writeFileSync(tmp, JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), updatedBy: by, values }, null, 1), { mode: 0o600 }); renameSync(tmp, file); Object.assign(cfg, patch); }, }; return cfg; }