* env : 같은 뜻의 환경 변수 — apply() 가 이 이름으로 process.env 에 넣고, migrateFromEnv() 가 이 이름에서 읽어 온다
* type : text | url | int | bool | enum | list(쉼표 목록) | path | secret
* default : 비어 있을 때의 동작 값(화면에 "기본값" 으로 보임). 코드 쪽 기본값과 같게 둔다
* deployTime: 배포 때 정해지는 값 — 환경 변수가 우선, 화면에는 읽기 전용으로 보인다
* restart : 바꾼 뒤 서비스를 재시작해야 적용되는 값 (모듈이 기동 때 한 번 읽음)
* secret : 값을 암호화 보관소에 두는 항목 (vaultKind 가 보관소 종류)
*/
constSCHEMA=[
{group:'service',label:'서비스',desc:'이 서비스 자체의 주소와 관리 설정입니다. 설치 직후 공개 주소를 먼저 확인하세요.',keys:[
{key:'service.publicBaseUrl',env:'PUBLIC_BASE_URL',type:'url',label:'서비스 공개 주소',desc:'사용자가 접속하는 주소(https://webmaker.example.com). 생성 사이트의 위젯·통계 스크립트·웹메일 안내 파일이 이 주소를 부른다. 틀리면 사이트의 문의폼·게시판이 동작하지 않는다.',default:'http://192.168.20.66',restart:true,required:true},
{key:'service.formBaseUrl',env:'FORM_BASE_URL',type:'url',label:'폼 접수 서버 주소',desc:'생성 사이트의 폼이 호출하는 서버. 비우면 공개 주소와 같다(관리자 콘솔 › 설정 › 메일 발송의 값이 있으면 그것이 우선).',default:'',restart:true},
{key:'service.adminUsers',env:'ADMIN_USERS',type:'list',label:'관리자로 취급할 아이디',desc:'회원 role 과 무관하게 관리자로 보는 아이디(쉼표 구분). 보통 비워 두고 관리자 콘솔(또는 scripts/admin.js grant)로 권한을 준다.',default:'',restart:true},
{key:'service.systemdUnit',env:'WEBMAKER_SERVICE',type:'text',label:'systemd 서비스 이름',desc:'초기 설정 페이지의 "서비스 재시작" 이 재시작할 유닛 이름(webmaker / webhosting-agent). 설치 스크립트가 넣는다.',default:''},
// v1.5.11 (요구 2): 기본 포트 3081 → 8795, 바인드 lan → 0.0.0.0. FQDN 을 모르는 서버에서도 http://<서버 IP>:8795/ 가 "기본 접속 위치" 가 되어 모든 설정을 웹에서 하도록.
// 포트가 겹치면 setup-server.js 가 다음 포트(8796, 8797 …)를 잡고 이 값에 적어 둔다. 접속은 토큰(data/setup.token) 또는 관리자 로그인이 필요하다.
{key:'service.setupPort',env:'WEBMAKER_SETUP_PORT',type:'int',min:1024,max:65535,label:'초기 설정 페이지 포트',desc:'설정 전용 웹페이지(setup-server.js)가 여는 포트. 기본 8795 — 겹치면 다음 포트(8796 …)를 자동으로 쓰고 여기에 기록한다. 바꾸면 webmaker-setup 서비스를 재시작한다. 같은 화면이 서비스 안 /setup 에도 있다.',default:8795},
{key:'service.setupBind',env:'WEBMAKER_SETUP_BIND',type:'text',label:'초기 설정 페이지 바인드 주소',desc:'0.0.0.0(기본: 모든 인터페이스 — 공인 IP 만 있는 서버에서도 http://<IP>:8795/ 로 접속), lan(이 서버의 첫 사설 IPv4 만), 또는 특정 IP. 접속에는 토큰(data/setup.token) 또는 관리자 로그인이 필요하고 로그인 시도는 15분 10회로 제한된다.',default:'0.0.0.0'},
{key:'service.setupRedirect',env:'WEBMAKER_SETUP_REDIRECT',type:'enum',options:[['auto','자동 (설치 점검표가 끝나기 전에는 첫 화면을 설정 페이지로)'],['off','끔 (항상 로그인 화면)']],label:'첫 화면을 설정 페이지로 보내기',desc:'관리자 비밀번호가 기본값이거나 AI 엔진 인증이 없어 운영할 수 없는 동안에는 서비스 첫 화면(/, /login)을 /setup 으로 보낸다. 점검표가 모두 끝나면 보내지 않는다. 점검 전에 로그인 화면을 열려면 /login?direct=1',default:'auto'},
// v1.6.31 (요구 5·7): 신뢰 프록시 · 내부 주소 허용 목록 · 생성 사이트 전용 주소
{key:'service.trustedProxies',env:'WM_TRUSTED_PROXIES',type:'text',label:'신뢰 프록시 IP(쉼표)',desc:'이 주소에서 온 연결의 X-Forwarded-For 만 클라이언트 IP 로 믿는다(속도 제한·기록용). 기본은 같은 서버의 nginx(127.0.0.1, ::1). 앞단 프록시가 다른 서버면 그 IP 를 적는다',default:''},
{key:'service.internalAllow',env:'WM_INTERNAL_ALLOW',type:'text',label:'내부 주소 허용 목록 (host:port, 쉼표)',desc:'사용자가 넣는 주소(참고·복사·가져오기, 제공자 URL)는 사설·내부 주소로 연결하지 않는다. 관리자가 허용한 내부 서비스(host:port)만 예외. 설정 파일의 Gitea·Arimail 주소는 자동 허용',default:''},
{key:'service.siteOrigin',env:'WM_SITE_ORIGIN',type:'text',label:'생성 사이트 전용 주소 (선택)',desc:'v1.6.31: 생성 사이트(/<사이트ID>/)를 관리 화면과 다른 origin(예: https://sites.example.com)에서 열어 관리 세션 쿠키가 생성 사이트 페이지에 전달되지 않게 한다. 비우면 같은 주소에서 열고, 서버가 요청 출처(Referer·CSRF 토큰)로 세션 사용을 막는다. 그 주소의 nginx·인증서는 운영자가 준비한다',default:''},
{key:'service.allowSignup',env:'WEBMAKER_ALLOW_SIGNUP',type:'bool',label:'회원가입 허용',desc:'끄면 로그인 화면에 회원가입 탭이 사라지고 POST /api/auth/signup 이 403 "회원가입이 닫혀 있습니다" 로 응답한다. 관리자는 콘솔 › 사용자에서 계정을 만든다. (웹서버 설정으로 /api/auth/signup 을 막으면 안내 없는 403 이 되므로 이 값을 쓴다)',default:true,envTrue:'1',envFalse:'0'},
]},
{group:'runtime',label:'실행 환경 (배포 때 정해지는 값 — 읽기 전용)',desc:'설치 스크립트가 systemd 유닛에 정한 값입니다. 포트·경로는 웹서버(nginx/Caddy) 프록시 설정과 함께 바뀌어야 하므로 여기서는 보기만 하고, 바꾸려면 유닛과 웹서버 설정을 함께 고친 뒤 재시작합니다.',keys:[
{key:'runtime.sitesRoot',env:'SITES_ROOT',type:'path',label:'사이트 파일 경로',desc:'생성 사이트가 저장되는 폴더(웹서버가 /<사이트ID>/ 로 정적 서비스)',default:'/var/www/sites',deployTime:true},
{key:'runtime.dataRoot',env:'DATA_ROOT',type:'path',label:'데이터 경로',desc:'회원·사이트·설정·보관소 파일이 있는 폴더(기본 앱 폴더의 data/). 이 설정 파일도 여기에 있다',default:path.join(ROOT,'data'),deployTime:true},
{key:'runtime.statsLog',env:'STATS_LOG',type:'path',label:'접속 통계 로그',desc:'nginx 가 쓰는 사이트 접속 로그(wmstats 형식)',default:'/var/log/nginx/webmaker-sites.log',deployTime:true},
]},
{group:'ai',label:'AI 엔진',desc:'Claude Code·OpenAI Codex CLI 실행 파일과 실행 한도입니다. 인증(API 키·OAuth 토큰·서버 로그인)은 관리자 콘솔 › AI 연결 탭(v1.5.16, 이전 설정 › 모델 연결)에서 합니다.',keys:[
{key:'ai.claudeBin',env:'CLAUDE_BIN',type:'path',label:'claude 실행 파일',desc:'비우면 PATH 와 알려진 설치 위치(~/.local/bin, ~/.npm-global/bin)에서 자동 탐색',default:'',restart:true},
{key:'ai.codexBin',env:'CODEX_BIN',type:'path',label:'codex 실행 파일',desc:'비우면 자동 탐색',default:'',restart:true},
{key:'ai.claudeModel',env:'CLAUDE_MODEL',type:'text',label:'Claude 기본 모델(설치값)',desc:'관리자 콘솔 › 설정 › 내부 호출 모델을 정하기 전까지 쓰는 Claude 모델(연결 테스트에도 사용). 콘솔에서 정하면 그 값이 우선',default:'claude-opus-5-5',restart:true},
{key:'ai.claudeMaxTurns',env:'CLAUDE_MAX_TURNS',type:'int',min:1,max:500,label:'Claude 최대 턴',desc:'한 작업에서 Claude 가 도구를 쓰는 횟수 상한',default:80,restart:true},
{key:'ai.openaiMaxTurns',env:'OPENAI_ENGINE_MAX_TURNS',type:'int',min:1,max:500,label:'OpenAI 호환 엔진 최대 턴',desc:'API 키로 쓰는 OpenAI 호환 모델의 도구 호출 상한',default:60,restart:true},
// v1.6.31 (검토 S8): 기본값 끔 — 켜면 claude 자식 프로세스 전체의 TLS 검증이 꺼진다(중간자 위험). 사설 CA 는 NODE_EXTRA_CA_CERTS 로 등록하는 것이 바른 방법
{key:'ai.allowInsecureTls',env:'CLAUDE_ALLOW_INSECURE_TLS',type:'bool',label:'Claude 실행 때 TLS 검증 완화 (권장하지 않음)',desc:'켜면 claude 자식 프로세스의 모든 HTTPS 검증이 꺼진다. 사내 프록시(자체 서명 인증서)에서만, 그것도 NODE_EXTRA_CA_CERTS 로 CA 를 등록할 수 없을 때만 켠다',default:false,envTrue:'1',envFalse:'0',restart:true},
{key:'ai.codexUnsafe',env:'CODEX_UNSAFE',type:'bool',label:'codex 샌드박스 없는 실행 허용 (위험)',desc:'v1.6.31: 샌드박스를 만들 수 없을 때 기본은 작업 거부(fail-closed)다. 켜면 샌드박스 없이 실행한다 — 다른 사이트·서버 설정·인증 정보에 접근할 수 있으므로 진단 목적 외에는 켜지 않는다',default:false,envTrue:'1',envFalse:'0',restart:true},
{key:'ai.maxJobMinutes',env:'WM_MAX_JOB_MINUTES',type:'int',min:5,max:240,label:'AI 작업 최대 실행 시간(분)',desc:'v1.6.31: 한 작업(생성·수정·기획 단계)이 이 시간을 넘기면 중단한다. 15분 동안 출력이 없어도 중단 — 멈춘 작업이 서버·다른 사용자에게 영향을 주지 않게',default:45},
{key:'ai.maxConcurrentJobs',env:'WM_MAX_CONCURRENT_JOBS',type:'int',min:1,max:32,label:'동시 AI 작업 수(전체)',desc:'v1.6.31: 서버 전체에서 동시에 실행할 AI 작업 수. 넘으면 "잠시 후 다시" 로 거절한다',default:4},
{key:'ai.codexSandbox',env:'CODEX_SANDBOX',type:'enum',options:[['auto','자동 (커널이 허용하면 샌드박스, 아니면 없이)'],['workspace-write','항상 샌드박스 (작업 폴더만 쓰기)'],['none','항상 샌드박스 없이']],label:'codex 샌드박스',desc:'codex 의 Linux 샌드박스는 비특권 user namespace 가 필요하다. Ubuntu 24.04 기본(kernel.apparmor_restrict_unprivileged_userns=1)에서는 샌드박스 안의 쓰기가 모두 실패하므로 "자동" 은 그때 샌드박스 없이 실행한다. 샌드박스를 쓰려면 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 (영구: /etc/sysctl.d/60-codex-sandbox.conf) 뒤 재시작',default:'auto'},
]},
{group:'source',label:'소스 보관 연동 (Gitea)',desc:'사이트의 생성·수정 버전을 보관하는 git 서비스입니다. 연동 모듈 lib/connectors/gitea.js 가 이 값으로 접속한다. deploy/install-gitea.sh 가 서비스 서버 안에 전용 Gitea 를 만들고 이 값을 채운다.',keys:[
{key:'source.provider',env:'SOURCE_PROVIDER',type:'enum',options:[['gitea','Gitea'],['none','연동 안 함 (로컬 버전 기록만)']],label:'연동 모듈',desc:'다른 git 서비스를 쓰려면 lib/connectors/ 에 모듈을 추가하고 여기서 고른다',default:'gitea'},
{key:'source.remote',env:'SITES_GIT_REMOTE',type:'text',label:'푸시 주소 (git 원격)',desc:'예: git@webmaker-gitea:webmaker/sites.git — 서비스 서버의 SSH 키가 그 계정에 등록되어 있어야 한다. 비우면 원격에 보관하지 않는다',default:''},
{key:'source.web',env:'SITES_GIT_WEB',type:'url',label:'웹 주소',desc:'운영자가 브라우저로 브랜치를 볼 주소(예: http://192.168.20.23:3030/webmaker/sites). 관리자 콘솔의 버전 링크에 쓴다',default:''},
{key:'source.prefix',env:'SITES_GIT_PREFIX',type:'text',label:'브랜치 접두어',desc:'사이트 브랜치 이름 <접두어>/<사이트ID>/main',default:'sites'},
{key:'source.apiUrl',env:'GITEA_API_URL',type:'url',label:'Gitea API 주소',desc:'예: http://192.168.20.23:3030 — 상태 확인·저장소 만들기·SSH 키 등록을 API 로 하려면 넣는다(선택). 토큰은 아래 비밀값',default:''},
{key:'source.token',type:'secret',vaultKind:'gitea',label:'Gitea API 토큰',desc:'위 계정의 접근 토큰(repo·user 권한). install-gitea.sh 가 만들어 넣는다. 없으면 API 기능(저장소 만들기·키 등록)만 꺼지고 푸시는 SSH 키로 그대로 된다'},
// [Ariweb Suite 변경] Ariweb Suite 포털 연동: 포털 주소(비밀 아님)와 공유 비밀(보관소 'ariweb'). lib/ariweb.js 참고.
{group:'ariweb',label:'Ariweb Suite 연동',desc:'Ariweb Suite 고객 포털과의 연동(별도 로그인 없는 제작 화면 진입, 프로젝트 ↔ 운영 사이트 연결). 포털의 보관소 sso:webmaker 와 같은 공유 비밀을 넣습니다.',keys:[
{key:'ariweb.portalUrl',env:'ARIWEB_PORTAL_URL',type:'url',label:'포털 공개 주소',desc:'예: https://my.ariweb.net — 발행 안내·폐기 영향 조회에 쓴다',default:''},
{key:'ariweb.secret',type:'secret',vaultKind:'ariweb',label:'공유 비밀',desc:'SSO 티켓·서비스 호출 서명에 쓰는 공유 비밀(암호화 보관). 포털이 발급한 값을 그대로 넣는다'},
{group:'mail',label:'메일 연동 (Arimail)',desc:'Arimail(구 mail-gateway)은 받는 메일·웹메일·IMAP/POP/SMTP 를 맡는 메일 서버입니다. 연동 모듈 lib/connectors/arimail.js 가 관리자 API 로 사이트 관리자 계정의 메일함을 만들고 지운다. 보내기(Resend 키)는 관리자 콘솔 › 설정 › 메일 발송 그대로.',keys:[
{key:'mail.provider',env:'MAIL_PROVIDER',type:'enum',options:[['none','연동 안 함 (보내기만)'],['arimail','Arimail']],label:'연동 모듈',desc:'다른 메일 서버를 쓰려면 lib/connectors/ 에 모듈을 추가하고 여기서 고른다',default:'none'},
{key:'mail.url',env:'ARIMAIL_URL',type:'url',label:'Arimail 주소 (공개)',desc:'사용자에게 안내하는 웹메일 주소(예: https://mail.example.com). deploy/install-arimail.sh 가 설치하면 자동으로 채운다',default:''},
{key:'mail.apiUrl',env:'ARIMAIL_API_URL',type:'url',label:'Arimail API 주소 (내부)',desc:'웹메이커 서버가 계정을 만들고 지울 때 부르는 주소. 같은 서버에 설치했으면 http://127.0.0.1:8790 처럼 로컬 주소를 넣어 인증서·DNS 와 무관하게 동작시킨다. 비우면 공개 주소를 쓴다',default:''},
{key:'mail.domain',env:'ARIMAIL_DOMAIN',type:'text',label:'메일 도메인',desc:'Arimail 이 다루는 도메인(계정은 아이디@이 도메인). 사이트의 메일 도메인이 이 값과 같을 때 받는 메일·웹메일이 연결된다',default:''},
{key:'mail.adminLogin',env:'ARIMAIL_ADMIN_LOGIN',type:'text',label:'관리자 로그인 ID',desc:'Arimail 관리자 페이지(/admin) 계정. 계정을 만들고 지우는 데 쓴다',default:'admin'},
{key:'mail.adminPassword',type:'secret',vaultKind:'arimail',label:'관리자 비밀번호',desc:'위 계정의 비밀번호(암호화 보관). install-arimail.sh 가 설치할 때 무작위로 정해 넣는다'},
// v1.6.38 (요구 2): 메일 서버를 여러 대 등록할 수 있다(관리자 콘솔 › 메일 시스템 › 메일 서버 연동, lib/mailservers.js). 여기 mail.* 는 그중 "기본 서버" 이며 닉네임·IP 는 고를 때 구분용
{key:'mail.nickname',env:'ARIMAIL_NICKNAME',type:'text',label:'기본 메일 서버 닉네임',desc:'관리자 콘솔에서 메일 서버를 고를 때 보이는 이름(예: ibit 메일). 비우면 호스트 이름',default:''},
{key:'mail.ip',env:'ARIMAIL_IP',type:'text',label:'기본 메일 서버 IP',desc:'표시·구분용 서버 IP(예: 115.68.194.192). 다른 서버에 설치한 Arimail 을 구분하기 위한 값이며 접속에는 주소(mail.url / mail.apiUrl)를 쓴다',default:''},
]},
{group:'ssl',label:'SSL (와일드카드 인증서)',desc:'플랫폼 도메인 와일드카드 인증서(관리자 콘솔 › 설정 › SSL)를 받을 때 쓰는 값입니다.',keys:[
{key:'ssl.acmeDir',env:'WM_ACME_DIR',type:'path',label:'ACME 작업 폴더',desc:'DNS TXT 값을 주고받는 임시 폴더',default:'/var/tmp/webmaker-acme',restart:true},
{key:'ssl.acmeWaitMin',env:'WM_ACME_WAIT_MIN',type:'int',min:5,max:600,label:'수동 DNS 대기(분)',desc:'수동 DNS 방식에서 운영자가 TXT 를 넣을 때까지 기다리는 최대 시간',default:40,restart:true},
]},
{group:'limits',label:'가져오기·복사 한도',desc:'기존 사이트 가져오기·복사의 상한입니다.',keys:[
{key:'limits.copyMaxPages',env:'COPY_MAX_PAGES',type:'int',min:1,max:100000,label:'최대 페이지 수',desc:'HTML 페이지 상한(무한 링크 방지)',default:300,restart:true},
{key:'limits.copyMaxAssets',env:'COPY_MAX_ASSETS',type:'int',min:1,max:1000000,label:'최대 자원 파일 수',desc:'이미지·CSS·JS 등 자원 파일 상한',default:5000,restart:true},
]},
{group:'endpoints',label:'외부 API 주소 (고급)',desc:'보통 바꾸지 않습니다. 시험용 대체 서버를 쓸 때만 넣습니다. 접속 키는 관리자 콘솔 › 설정 › 외부 연동 접속 정보에 있습니다.',advanced:true,keys:[
case'url':{constv=String(s);if(!/^https?:\/\/[^\s/]+/i.test(v))throwhttpError(`${def.label}은(는) http:// 또는 https:// 로 시작하는 주소여야 합니다`);returnv.replace(/\/+$/,'');}
case'path':{constv=String(s);if(!v.startsWith('/'))throwhttpError(`${def.label}은(는) / 로 시작하는 절대 경로여야 합니다`);returnv;}
case'enum':{constv=String(s);if(!def.options.some((o)=>o[0]===v))throwhttpError(`${def.label}: 고를 수 없는 값입니다 (${def.options.map((o)=>o[0]).join(', ')})`);returnv;}
functionunset(key,by='cli'){constdef=KEYS[key];if(!def)throwhttpError(`알 수 없는 설정 항목: ${key}`);constd=load();constvalues={...d.values};deletevalues[key];save({values},by);if(def.env&&def.type!=='secret')deleteprocess.env[def.env];returntrue;}