실제 환경 배포 결함 수정 4: AriMail 설정 API Host(setup_host), Agent heartbeat 의 REVISION 버전 보고, 새 유닛 enable
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0862f6fae9
commit
23189f4dfb
@@ -106,6 +106,9 @@
|
||||
- **부트스트랩 릴리스 기록**(`hal/core.mjs setBootstrap`): HAL 기동 전 CLI `bootstrap-release` 가 쓴 UPDATE 가 행(id=1)이 없어 무효였다(건강 응답의 `releaseVersion` 이 null). 행을 먼저 만든다(`test/hal.test.mjs` 회귀 테스트).
|
||||
- **AriMail 마이그레이션의 EnvironmentFile 읽기**(`deploy/module-as-user.sh migrate`): systemd 문법(`MAIL_FROM_NAME=Ari Hak dang` 처럼 따옴표 없는 공백 값)을 bash 로 소스하다 실패했다. 줄 단위로 읽어 따옴표를 벗기고 export 한다.
|
||||
- **사후 절차 실패 시 되돌리기**(`agent/jobs/module.mjs`): current 전환 뒤 마이그레이션이 실패하면 current 가 새 릴리스를 가리킨 채 남았다(다음 재시작 때 마이그레이션 없는 새 코드가 뜬다). 실패하면 이전 릴리스로 되돌리고 재시작한다.
|
||||
- **AriMail 전체 설정 API 의 Host**(`portal/mail/arimail.mjs`, `mail_servers.setup_host`): AriMail 은 설정 API(`/api/setup/*`)를 기본 도메인 호스트(예: `mail.arihakdang.com`)에서만 응답하고 다른 테넌트 호스트로는 404 를 준다. 설정 주소(`setup_url`, 인증서 이름과 내부 접속용)와 별도로 `setupHost` 를 등록해 Host 헤더로 보낸다.
|
||||
- **Agent heartbeat 의 모듈 버전**(`agent/index.mjs`): `current/package.json` 버전(상류 1.7.3·1.0.11)을 보고해 HAL 이 배포 직후 "목표와 다름(drift)" 으로 표시했다. `REVISION.json` 의 Suite 버전을 먼저 읽는다.
|
||||
- **새 유닛 enable**(`agent/jobs/module.mjs`): 템플릿으로 만든 유닛(포털)을 enable 하지 않아 재부팅 뒤 뜨지 않았을 것이다. 유닛을 쓸 때 `systemctl enable` 한다.
|
||||
- 회귀 테스트: `test/agent.test.mjs` 에 실행 계정 모드(도우미 스크립트 실제 실행·경로 제한 거부)·REVISION 버전·기존 유닛 보존 검사를 추가했다.
|
||||
|
||||
### 도입 소스
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ if (cmd === 'register') {
|
||||
const client = createClient({ halUrl: cfg.halUrl, agentId: cfg.id, secret: cfg.vault().get('agent-secret') });
|
||||
const runner = createRunner({ kinds: KINDS, roots: cfg.roots, sys, client, dataDir: cfg.dataDir, log });
|
||||
let stopping = false; let running = 0; let lastFacts = null; let restartUnit = null;
|
||||
/** 모듈 상태(heartbeat): facts 의 유닛이 살아 있는지 + current/package.json 버전 */
|
||||
async function moduleStatus(f) { const out = {}; for (const [m, unit] of Object.entries(f.units || {})) { const appDir = (f.appDirs || {})[m] || join(cfg.roots.srv, m); const cur = join(appDir, 'current', 'package.json'); let version = null; try { version = JSON.parse(readFileSync(cur, 'utf8')).version; } catch (_) {} const active = await sys.isActive(unit); out[m] = { version, active, health: active ? 'ok' : 'down' }; } out.agent = { version: VERSION, active: true, health: 'ok' }; out.observability = { version: VERSION, active: true, health: 'ok' }; if (existsSync(cfg.roots.backups)) out.backup = { version: VERSION, active: true, health: 'ok' }; return out; }
|
||||
/** 모듈 상태(heartbeat): facts 의 유닛이 살아 있는지 + current/REVISION.json(없으면 package.json) 버전 */
|
||||
async function moduleStatus(f) { const out = {}; for (const [m, unit] of Object.entries(f.units || {})) { const appDir = (f.appDirs || {})[m] || join(cfg.roots.srv, m); let version = null; try { version = JSON.parse(readFileSync(join(appDir, 'current', 'REVISION.json'), 'utf8')).version; } catch (_) {} if (!version) { try { version = JSON.parse(readFileSync(join(appDir, 'current', 'package.json'), 'utf8')).version; } catch (_) {} } // 도입 모듈은 REVISION.json 의 Suite 버전(상류 package.json 버전은 목표와 다르다 — 실제 환경 결함 수정) const active = await sys.isActive(unit); out[m] = { version, active, health: active ? 'ok' : 'down' }; } out.agent = { version: VERSION, active: true, health: 'ok' }; out.observability = { version: VERSION, active: true, health: 'ok' }; if (existsSync(cfg.roots.backups)) out.backup = { version: VERSION, active: true, health: 'ok' }; return out; }
|
||||
async function heartbeat() { try { if (!lastFacts || Date.now() - lastFacts.at > 10 * 60000) lastFacts = { at: Date.now(), f: facts(sys) }; const sites = existsSync(cfg.roots.sites) ? readdirSync(cfg.roots.sites).filter((x) => existsSync(join(cfg.roots.sites, x, 'current'))).length : 0; await client.request('POST', '/agent/v1/heartbeat', { version: VERSION, metrics: sample(cfg.roots), modules: await moduleStatus(lastFacts.f), clockSkewMs: client.clockSkewMs, facts: lastFacts.f, sitesHosted: sites, jobs: { running } }); } catch (e) { log(`[agent] heartbeat 실패: ${e.message}`); } }
|
||||
async function execute(job) { running++; try { const out = await runner.execute(job); if (out?.result?.restartAfterReport) restartUnit = out.result.restartAfterReport; } finally { running--; } if (restartUnit) { const u = restartUnit; restartUnit = null; log(`[agent] 자기 갱신 — ${u} 재시작`); setTimeout(() => sys.systemctl('restart', u, { optional: true }), 1500); } }
|
||||
// 재시작 뒤: 보고 못 한 결과 전송, 미완료 작업 재개
|
||||
|
||||
@@ -63,7 +63,7 @@ async function writeUnit(ctx, desc, unit, appDir, version, svc) {
|
||||
const envLines = old.split('\n').filter((l) => /^Environment=/.test(l) && !/^Environment=(NODE_ENV|HOST|PORT|ARIWEB_[A-Z_]+_DATA|NODE_OPTIONS)=/.test(l)).join('\n');
|
||||
const user = svc.user || desc.user || process.env.USER || 'master'; const envFile = svc.envFile || ''; const node = svc.nodeBin || process.execPath;
|
||||
const txt = render(ctx.payload.unitTemplate, { APP_DIR: appDir, USER: user, NODE: node, ENV_LINES: envLines, VERSION: version, UNIT: unit, ENV_FILE: envFile });
|
||||
await ctx.sys.writeFile(`/etc/systemd/system/${unit}.service`, txt); await ctx.sys.systemctl('daemon-reload', '', { optional: true }); return { written: true, preservedEnv: envLines.split('\n').filter(Boolean).length };
|
||||
await ctx.sys.writeFile(`/etc/systemd/system/${unit}.service`, txt); await ctx.sys.systemctl('daemon-reload', '', { optional: true }); const en = await ctx.sys.systemctl('enable', unit, { optional: true }); /* 새로 만든 유닛은 재부팅 뒤에도 뜨도록 enable(실제 환경 결함 수정: 포털 유닛이 disabled 였다) */ return { written: true, enabled: en.code === 0, preservedEnv: envLines.split('\n').filter(Boolean).length };
|
||||
}
|
||||
/** EnvironmentFile(KEY=값 줄) 읽기 — 직접 모드의 마이그레이션에 싣는다. 값은 돌려주되 로그에는 쓰지 않는다. */
|
||||
function readEnvFile(p) { const out = {}; if (!p || !existsSync(p)) return out; for (const line of readFileSync(p, 'utf8').split('\n')) { const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line); if (!m) continue; let v = m[2].trim(); if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); out[m[1]] = v; } return out; }
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"generatedAt": "2026-10-26T13:59:08.928Z",
|
||||
"startedAt": "2026-10-26T13:56:48.040Z",
|
||||
"generatedAt": "2026-10-26T14:06:08.722Z",
|
||||
"startedAt": "2026-10-26T14:03:49.276Z",
|
||||
"env": {
|
||||
"agents": 4,
|
||||
"ports": {
|
||||
"hal": 41117,
|
||||
"portal": 46025,
|
||||
"wm": 46747,
|
||||
"am": 40149,
|
||||
"mail": 35967
|
||||
"hal": 45603,
|
||||
"portal": 36257,
|
||||
"wm": 33489,
|
||||
"am": 42349,
|
||||
"mail": 35743
|
||||
},
|
||||
"mailServerRegistered": true,
|
||||
"node": "v24.22.1"
|
||||
@@ -99,12 +99,12 @@
|
||||
{
|
||||
"name": "팀 그룹 만들기 → 현재 그룹 전환",
|
||||
"ok": true,
|
||||
"detail": "g_01a1787ff02514257de360368a"
|
||||
"detail": "g_01a178865c385db5f6ac357767"
|
||||
},
|
||||
{
|
||||
"name": "이메일 초대(7일) → 초대 메일 링크",
|
||||
"ok": true,
|
||||
"detail": "kL-VA6kLTcY9-9NNNN6LxXpXqWNXwskg"
|
||||
"detail": "YM3hdbeCEOx2z_Gg4jmKkLWTdZrFJ2PH"
|
||||
},
|
||||
{
|
||||
"name": "초대 수락(1회용) → 별칭 user-02 · 역할 member",
|
||||
@@ -162,7 +162,7 @@
|
||||
"detail": {
|
||||
"count": 2,
|
||||
"team": {
|
||||
"id": "pv_01a1787fed98a2869a7e65ec01",
|
||||
"id": "pv_01a178865ace62768069867677",
|
||||
"price": 49000,
|
||||
"sites": 3
|
||||
},
|
||||
@@ -179,7 +179,7 @@
|
||||
"orderNo": "AW-20261026-0001",
|
||||
"status": "awaiting_payment",
|
||||
"total": 63800,
|
||||
"dueAt": "2026-11-02T13:56:53.016Z",
|
||||
"dueAt": "2026-11-02T14:03:53.905Z",
|
||||
"hasInstructions": true
|
||||
}
|
||||
},
|
||||
@@ -221,7 +221,7 @@
|
||||
"detail": {
|
||||
"sites": 3,
|
||||
"subs": 1,
|
||||
"nextBilling": "2026-11-26T13:56:53.082Z"
|
||||
"nextBilling": "2026-11-26T14:03:53.969Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -233,7 +233,7 @@
|
||||
"bill": {
|
||||
"sites": 3,
|
||||
"subs": 1,
|
||||
"nextBilling": "2026-11-26T13:56:53.082Z"
|
||||
"nextBilling": "2026-11-26T14:03:53.969Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -248,7 +248,7 @@
|
||||
"name": "사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "s_01a1787ff118784dbb14dd9328",
|
||||
"id": "s_01a178865d31eb6cc616c911ab",
|
||||
"lifecycle": "draft",
|
||||
"projectId": "00000000000000000000000000000001",
|
||||
"nextSteps": [
|
||||
@@ -283,7 +283,7 @@
|
||||
"name": "제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용)",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"portalRedirect": "http://127.0.0.1:46747/api/auth/sso?ticket=…",
|
||||
"portalRedirect": "http://127.0.0.1:33489/api/auth/sso?ticket=…",
|
||||
"webmaker": 302,
|
||||
"reuse": 401
|
||||
}
|
||||
@@ -299,7 +299,7 @@
|
||||
],
|
||||
"evidence": {
|
||||
"sso": {
|
||||
"portalRedirect": "http://127.0.0.1:46747/api/auth/sso?ticket=…",
|
||||
"portalRedirect": "http://127.0.0.1:33489/api/auth/sso?ticket=…",
|
||||
"webmaker": 302,
|
||||
"reuse": 401
|
||||
},
|
||||
@@ -320,7 +320,7 @@
|
||||
"name": "배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"jobId": "j_01a1787ff15708325bb3f4f76a",
|
||||
"jobId": "j_01a178865d7c302bfebc85692e",
|
||||
"steps": 10,
|
||||
"again": false,
|
||||
"siteCount": "reserved",
|
||||
@@ -351,7 +351,7 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"lifecycle": "needs_domain",
|
||||
"version": "20261026135653-rev-c1-000000",
|
||||
"version": "20261026140355-rev-c1-000000",
|
||||
"verify": "needs_domain",
|
||||
"health": "ok",
|
||||
"agentHasSite": true
|
||||
@@ -360,7 +360,7 @@
|
||||
],
|
||||
"evidence": {
|
||||
"d": {
|
||||
"jobId": "j_01a1787ff15708325bb3f4f76a",
|
||||
"jobId": "j_01a178865d7c302bfebc85692e",
|
||||
"steps": 10,
|
||||
"again": false,
|
||||
"siteCount": "reserved",
|
||||
@@ -383,7 +383,7 @@
|
||||
},
|
||||
"after": {
|
||||
"lifecycle": "needs_domain",
|
||||
"version": "20261026135653-rev-c1-000000",
|
||||
"version": "20261026140355-rev-c1-000000",
|
||||
"verify": "needs_domain",
|
||||
"health": "ok",
|
||||
"agentHasSite": true
|
||||
@@ -401,7 +401,7 @@
|
||||
"name": "도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT)",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"domainId": "d_01a17880012bc25aa36fca86f9",
|
||||
"domainId": "d_01a178866d50f0bd814ff16339",
|
||||
"txtName": "_ariweb-verify.aw-e2e.localhost",
|
||||
"txtValueShape": "ariweb-verify=…",
|
||||
"copyable": true
|
||||
@@ -422,7 +422,7 @@
|
||||
],
|
||||
"evidence": {
|
||||
"dm": {
|
||||
"domainId": "d_01a17880012bc25aa36fca86f9",
|
||||
"domainId": "d_01a178866d50f0bd814ff16339",
|
||||
"txtName": "_ariweb-verify.aw-e2e.localhost",
|
||||
"txtValueShape": "ariweb-verify=…",
|
||||
"copyable": true
|
||||
@@ -500,7 +500,7 @@
|
||||
"name": "사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "msv_01a1788089b2517001dca93c90",
|
||||
"id": "msv_01a17886f5eea0a3261b10719a",
|
||||
"status": "needs_setup",
|
||||
"tenant": true,
|
||||
"ariwebKind": "site_bound",
|
||||
@@ -517,7 +517,7 @@
|
||||
"name": "메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "mb_01a1788089ea344ac20fff0cff",
|
||||
"id": "mb_01a17886f6282d11bf30593b13",
|
||||
"address": "kim@aw-e2e.localhost",
|
||||
"passwordShownOnce": "[redacted]",
|
||||
"mustChange": true,
|
||||
@@ -584,7 +584,7 @@
|
||||
],
|
||||
"evidence": {
|
||||
"svc": {
|
||||
"id": "msv_01a1788089b2517001dca93c90",
|
||||
"id": "msv_01a17886f5eea0a3261b10719a",
|
||||
"status": "needs_setup",
|
||||
"tenant": true,
|
||||
"ariwebKind": "site_bound",
|
||||
@@ -592,7 +592,7 @@
|
||||
"siteCountAllocated": 0
|
||||
},
|
||||
"mb": {
|
||||
"id": "mb_01a1788089ea344ac20fff0cff",
|
||||
"id": "mb_01a17886f6282d11bf30593b13",
|
||||
"address": "kim@aw-e2e.localhost",
|
||||
"passwordShownOnce": "[redacted]",
|
||||
"mustChange": true,
|
||||
@@ -644,7 +644,7 @@
|
||||
"name": "메일 전용 도메인 등록(solo-e2e.localhost, purpose mail)",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "d_01a17880affa55291738680a3a",
|
||||
"id": "d_01a17887203cc05d2e6538d60b",
|
||||
"jobCancelled": null
|
||||
}
|
||||
},
|
||||
@@ -652,7 +652,7 @@
|
||||
"name": "독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비)",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "msv_01a17880b003f52685dc3a1e83",
|
||||
"id": "msv_01a1788720453f9268e2f9c599",
|
||||
"kind": "standalone",
|
||||
"mailServicesAllocated": 1,
|
||||
"sitesAllocated": 0
|
||||
@@ -661,7 +661,7 @@
|
||||
],
|
||||
"evidence": {
|
||||
"r": {
|
||||
"id": "msv_01a17880b003f52685dc3a1e83",
|
||||
"id": "msv_01a1788720453f9268e2f9c599",
|
||||
"kind": "standalone",
|
||||
"mailServicesAllocated": 1,
|
||||
"sitesAllocated": 0
|
||||
@@ -701,8 +701,8 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"status": "active",
|
||||
"firstDue": "2026-10-27T13:57:42.172Z",
|
||||
"nextRun": "2026-10-26T13:57:42.172Z"
|
||||
"firstDue": "2026-10-27T14:04:44.126Z",
|
||||
"nextRun": "2026-10-26T14:04:44.126Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -712,7 +712,7 @@
|
||||
"timedOut": false,
|
||||
"optionStatus": "active",
|
||||
"lastJob": null,
|
||||
"siteServer": "srv_01a1787fe74fe122af4bf99ed5",
|
||||
"siteServer": "srv_01a1788654a32b53cee3e1c344",
|
||||
"backup": {
|
||||
"status": "done",
|
||||
"tier": "monthly",
|
||||
@@ -733,14 +733,14 @@
|
||||
},
|
||||
"opt": {
|
||||
"status": "active",
|
||||
"firstDue": "2026-10-27T13:57:42.172Z",
|
||||
"nextRun": "2026-10-26T13:57:42.172Z"
|
||||
"firstDue": "2026-10-27T14:04:44.126Z",
|
||||
"nextRun": "2026-10-26T14:04:44.126Z"
|
||||
},
|
||||
"run1": {
|
||||
"timedOut": false,
|
||||
"optionStatus": "active",
|
||||
"lastJob": null,
|
||||
"siteServer": "srv_01a1787fe74fe122af4bf99ed5",
|
||||
"siteServer": "srv_01a1788654a32b53cee3e1c344",
|
||||
"backup": {
|
||||
"status": "done",
|
||||
"tier": "monthly",
|
||||
@@ -763,19 +763,19 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"backup": {
|
||||
"id": "bk_01a17880c4e1bdfcb12b9e2585",
|
||||
"createdAt": "2026-10-26T13:57:42.179Z",
|
||||
"id": "bk_01a178873211d14d742b283b95",
|
||||
"createdAt": "2026-10-26T14:04:44.134Z",
|
||||
"scope": {
|
||||
"files": true,
|
||||
"db": true,
|
||||
"uploads": true,
|
||||
"config": true,
|
||||
"storeServerId": "srv_01a1787fe7fd6b1a739fe34322",
|
||||
"storeServerId": "srv_01a17886555747e79cb48c2573",
|
||||
"verifyMethod": "sha256 + 격리 환경 읽기 확인"
|
||||
},
|
||||
"sizeBytes": 1907,
|
||||
"sizeBytes": 1895,
|
||||
"verifyState": "verified",
|
||||
"expiresAt": "2027-01-27T13:57:47.362Z",
|
||||
"expiresAt": "2027-01-27T14:04:48.529Z",
|
||||
"tier": "monthly"
|
||||
},
|
||||
"included": [
|
||||
@@ -809,7 +809,7 @@
|
||||
"name": "복원 요청 → 운영자 승인 대기 · 중복 요청 없음",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "rs_01a17880c8586bb0433c63ec1d",
|
||||
"id": "rs_01a1788732b6ab8b3f85488a93",
|
||||
"status": "awaiting_approval",
|
||||
"dup": false,
|
||||
"finalConfirmation": {
|
||||
@@ -838,19 +838,19 @@
|
||||
"evidence": {
|
||||
"info": {
|
||||
"backup": {
|
||||
"id": "bk_01a17880c4e1bdfcb12b9e2585",
|
||||
"createdAt": "2026-10-26T13:57:42.179Z",
|
||||
"id": "bk_01a178873211d14d742b283b95",
|
||||
"createdAt": "2026-10-26T14:04:44.134Z",
|
||||
"scope": {
|
||||
"files": true,
|
||||
"db": true,
|
||||
"uploads": true,
|
||||
"config": true,
|
||||
"storeServerId": "srv_01a1787fe7fd6b1a739fe34322",
|
||||
"storeServerId": "srv_01a17886555747e79cb48c2573",
|
||||
"verifyMethod": "sha256 + 격리 환경 읽기 확인"
|
||||
},
|
||||
"sizeBytes": 1907,
|
||||
"sizeBytes": 1895,
|
||||
"verifyState": "verified",
|
||||
"expiresAt": "2027-01-27T13:57:47.362Z",
|
||||
"expiresAt": "2027-01-27T14:04:48.529Z",
|
||||
"tier": "monthly"
|
||||
},
|
||||
"included": [
|
||||
@@ -880,7 +880,7 @@
|
||||
"deferred": []
|
||||
},
|
||||
"rq": {
|
||||
"id": "rs_01a17880c8586bb0433c63ec1d",
|
||||
"id": "rs_01a1788732b6ab8b3f85488a93",
|
||||
"status": "awaiting_approval",
|
||||
"dup": false,
|
||||
"finalConfirmation": {
|
||||
@@ -970,8 +970,8 @@
|
||||
"name": "외부 배포 사이트 생성(초안) + 저장본",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "s_01a17881b3cfe6725e494551cd",
|
||||
"group_id": "g_01a1787ff02514257de360368a",
|
||||
"id": "s_01a178881e3458f3e1da02398f",
|
||||
"group_id": "g_01a178865c385db5f6ac357767",
|
||||
"name": "E2E 외부 사이트",
|
||||
"purpose": "e2e",
|
||||
"webmaker_project_id": "00000000000000000000000000000004",
|
||||
@@ -999,9 +999,9 @@
|
||||
"retention_until": null,
|
||||
"delete_after": null,
|
||||
"deleted_at": null,
|
||||
"created_by": "u_01a1787fef810090e8c864240d",
|
||||
"created_at": "2026-10-26T13:58:48.527Z",
|
||||
"updated_at": "2026-10-26T13:58:48.533Z",
|
||||
"created_by": "u_01a178865b9219bc0553c703f8",
|
||||
"created_at": "2026-10-26T14:05:48.980Z",
|
||||
"updated_at": "2026-10-26T14:05:48.990Z",
|
||||
"runtime": "static",
|
||||
"consent_version": null,
|
||||
"pending_cleanup_json": null,
|
||||
@@ -1042,8 +1042,8 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"token": "[redacted]",
|
||||
"install": "curl -fsSL http://127.0.0.1:41117/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:41117 --token … --external --site s_01a17881b3cfe6725e494551cd",
|
||||
"expiresAt": "2026-10-26T14:08:48.809Z",
|
||||
"install": "curl -fsSL http://127.0.0.1:45603/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:45603 --token … --external --site s_01a178881e3458f3e1da02398f",
|
||||
"expiresAt": "2026-10-26T14:15:49.217Z",
|
||||
"status": "pending"
|
||||
}
|
||||
},
|
||||
@@ -1126,8 +1126,8 @@
|
||||
"agreed": false
|
||||
},
|
||||
"tok": {
|
||||
"install": "curl -fsSL http://127.0.0.1:41117/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:41117 --token … --external --site s_01a17881b3cfe6725e494551cd",
|
||||
"expiresAt": "2026-10-26T14:08:48.809Z"
|
||||
"install": "curl -fsSL http://127.0.0.1:45603/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:45603 --token … --external --site s_01a178881e3458f3e1da02398f",
|
||||
"expiresAt": "2026-10-26T14:15:49.217Z"
|
||||
},
|
||||
"conn": {
|
||||
"status": "connected",
|
||||
@@ -1311,7 +1311,7 @@
|
||||
"approve": 200,
|
||||
"sites": 1,
|
||||
"mailboxes": 5,
|
||||
"product": "pv_01a1787fed98b01207eddba3f9"
|
||||
"product": "pv_01a178865acd6d92fafdc54f8f"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1347,7 +1347,7 @@
|
||||
"approve": 200,
|
||||
"sites": 1,
|
||||
"mailboxes": 5,
|
||||
"product": "pv_01a1787fed98b01207eddba3f9"
|
||||
"product": "pv_01a178865acd6d92fafdc54f8f"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1430,9 +1430,9 @@
|
||||
"name": "요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "tr_01a17881d680c702999f932704",
|
||||
"id": "tr_01a178883968273245f1a10b7c",
|
||||
"status": "requested",
|
||||
"expiresAt": "2026-11-02T13:58:57.408Z",
|
||||
"expiresAt": "2026-11-02T14:05:55.944Z",
|
||||
"bobNotified": true
|
||||
}
|
||||
},
|
||||
@@ -1460,7 +1460,7 @@
|
||||
"bad": "final_confirmation_mismatch",
|
||||
"ok": 200,
|
||||
"status": "completed",
|
||||
"cutover": "2026-10-26T13:58:57.455Z"
|
||||
"cutover": "2026-10-26T14:05:55.994Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1489,9 +1489,9 @@
|
||||
"excluded": 3
|
||||
},
|
||||
"rq": {
|
||||
"id": "tr_01a17881d680c702999f932704",
|
||||
"id": "tr_01a178883968273245f1a10b7c",
|
||||
"status": "requested",
|
||||
"expiresAt": "2026-11-02T13:58:57.408Z",
|
||||
"expiresAt": "2026-11-02T14:05:55.944Z",
|
||||
"bobNotified": true
|
||||
},
|
||||
"acc": {
|
||||
@@ -1503,7 +1503,7 @@
|
||||
"bad": "final_confirmation_mismatch",
|
||||
"ok": 200,
|
||||
"status": "completed",
|
||||
"cutover": "2026-10-26T13:58:57.455Z"
|
||||
"cutover": "2026-10-26T14:05:55.994Z"
|
||||
},
|
||||
"after": {
|
||||
"aliceSees": 404,
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"detail": {
|
||||
"status": "done",
|
||||
"canRetry": false,
|
||||
"jobId": "j_01a1787ff15708325bb3f4f76a"
|
||||
"jobId": "j_01a178865d7c302bfebc85692e"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1806,7 +1806,7 @@
|
||||
"dep": {
|
||||
"status": "done",
|
||||
"canRetry": false,
|
||||
"jobId": "j_01a1787ff15708325bb3f4f76a"
|
||||
"jobId": "j_01a178865d7c302bfebc85692e"
|
||||
},
|
||||
"rt": {
|
||||
"status": 400,
|
||||
@@ -1832,7 +1832,7 @@
|
||||
"detail": {
|
||||
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
|
||||
"category": "billing",
|
||||
"link": "/orders/o_01a1787ff097f1475baf324276"
|
||||
"link": "/orders/o_01a178865cb1f3d04a833a14fa"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1848,7 +1848,7 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"status": "approved",
|
||||
"approvedAt": "2026-10-26T13:56:53.082Z"
|
||||
"approvedAt": "2026-10-26T14:03:53.969Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1856,7 +1856,7 @@
|
||||
"n": {
|
||||
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
|
||||
"category": "billing",
|
||||
"link": "/orders/o_01a1787ff097f1475baf324276"
|
||||
"link": "/orders/o_01a178865cb1f3d04a833a14fa"
|
||||
},
|
||||
"a": {
|
||||
"action": "order.approve",
|
||||
@@ -1864,7 +1864,7 @@
|
||||
},
|
||||
"same": {
|
||||
"status": "approved",
|
||||
"approvedAt": "2026-10-26T13:56:53.082Z"
|
||||
"approvedAt": "2026-10-26T14:03:53.969Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1907,9 +1907,9 @@
|
||||
"rpo_minutes": 30,
|
||||
"snapshot_interval_minutes": 60,
|
||||
"retention_count": 3,
|
||||
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026135648/dr-external",
|
||||
"updated_at": "2026-10-26T13:59:05.848Z",
|
||||
"updated_by": "op_01a1787fe64f62e76ecfed50f6"
|
||||
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026140349/dr-external",
|
||||
"updated_at": "2026-10-26T14:06:05.749Z",
|
||||
"updated_by": "op_01a1788653a9a3257ffd73d6d8"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1969,9 +1969,9 @@
|
||||
"rpo_minutes": 30,
|
||||
"snapshot_interval_minutes": 60,
|
||||
"retention_count": 3,
|
||||
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026135648/dr-external",
|
||||
"updated_at": "2026-10-26T13:59:05.848Z",
|
||||
"updated_by": "op_01a1787fe64f62e76ecfed50f6"
|
||||
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026140349/dr-external",
|
||||
"updated_at": "2026-10-26T14:06:05.749Z",
|
||||
"updated_by": "op_01a1788653a9a3257ffd73d6d8"
|
||||
},
|
||||
"snap": {
|
||||
"verify": "ok",
|
||||
@@ -2005,7 +2005,7 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"mailJob": {
|
||||
"id": "j_01a178809a29be75e2c1ee2f1b",
|
||||
"id": "j_01a178870a63a734460ce886f3",
|
||||
"status": "awaiting_external"
|
||||
},
|
||||
"halJobs": 18
|
||||
@@ -2017,7 +2017,7 @@
|
||||
"detail": {
|
||||
"sessionKept": "[redacted]",
|
||||
"mailJob": {
|
||||
"id": "j_01a178809a29be75e2c1ee2f1b",
|
||||
"id": "j_01a178870a63a734460ce886f3",
|
||||
"status": "awaiting_external"
|
||||
},
|
||||
"same": true
|
||||
@@ -2036,7 +2036,7 @@
|
||||
"evidence": {
|
||||
"before": {
|
||||
"mailJob": {
|
||||
"id": "j_01a178809a29be75e2c1ee2f1b",
|
||||
"id": "j_01a178870a63a734460ce886f3",
|
||||
"status": "awaiting_external"
|
||||
},
|
||||
"halJobs": 18
|
||||
@@ -2044,7 +2044,7 @@
|
||||
"pr": {
|
||||
"sessionKept": "[redacted]",
|
||||
"mailJob": {
|
||||
"id": "j_01a178809a29be75e2c1ee2f1b",
|
||||
"id": "j_01a178870a63a734460ce886f3",
|
||||
"status": "awaiting_external"
|
||||
},
|
||||
"same": true
|
||||
@@ -2075,8 +2075,8 @@
|
||||
"ok": true,
|
||||
"detail": [
|
||||
{
|
||||
"id": "s_01a178807db2350aa3520b04a8",
|
||||
"group_id": "g_01a1787ff051603dc8fd16c945",
|
||||
"id": "s_01a17886e9ee4810dcf115f705",
|
||||
"group_id": "g_01a178865c69eac738e0dbf93e",
|
||||
"name": "동시 A",
|
||||
"purpose": "e2e",
|
||||
"webmaker_project_id": "00000000000000000000000000000002",
|
||||
@@ -2104,9 +2104,9 @@
|
||||
"retention_until": null,
|
||||
"delete_after": null,
|
||||
"deleted_at": null,
|
||||
"created_by": "u_01a1787ff051a03ee09946e65a",
|
||||
"created_at": "2026-10-26T13:57:29.138Z",
|
||||
"updated_at": "2026-10-26T13:57:29.147Z",
|
||||
"created_by": "u_01a178865c698fdc51bec81244",
|
||||
"created_at": "2026-10-26T14:04:30.062Z",
|
||||
"updated_at": "2026-10-26T14:04:30.071Z",
|
||||
"runtime": "static",
|
||||
"consent_version": null,
|
||||
"pending_cleanup_json": null,
|
||||
@@ -2125,8 +2125,8 @@
|
||||
"nextJob": null
|
||||
},
|
||||
{
|
||||
"id": "s_01a178807db6d2d4338e856e55",
|
||||
"group_id": "g_01a1787ff051603dc8fd16c945",
|
||||
"id": "s_01a17886e9f227409700d9439c",
|
||||
"group_id": "g_01a178865c69eac738e0dbf93e",
|
||||
"name": "동시 B",
|
||||
"purpose": "e2e",
|
||||
"webmaker_project_id": "00000000000000000000000000000003",
|
||||
@@ -2154,9 +2154,9 @@
|
||||
"retention_until": null,
|
||||
"delete_after": null,
|
||||
"deleted_at": null,
|
||||
"created_by": "u_01a1787ff051a03ee09946e65a",
|
||||
"created_at": "2026-10-26T13:57:29.142Z",
|
||||
"updated_at": "2026-10-26T13:57:29.149Z",
|
||||
"created_by": "u_01a178865c698fdc51bec81244",
|
||||
"created_at": "2026-10-26T14:04:30.066Z",
|
||||
"updated_at": "2026-10-26T14:04:30.074Z",
|
||||
"runtime": "static",
|
||||
"consent_version": null,
|
||||
"pending_cleanup_json": null,
|
||||
@@ -2214,8 +2214,8 @@
|
||||
"name": "외부 배포 사이트 생성(초안) + 저장본",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "s_01a17881b3cfe6725e494551cd",
|
||||
"group_id": "g_01a1787ff02514257de360368a",
|
||||
"id": "s_01a178881e3458f3e1da02398f",
|
||||
"group_id": "g_01a178865c385db5f6ac357767",
|
||||
"name": "E2E 외부 사이트",
|
||||
"purpose": "e2e",
|
||||
"webmaker_project_id": "00000000000000000000000000000004",
|
||||
@@ -2243,9 +2243,9 @@
|
||||
"retention_until": null,
|
||||
"delete_after": null,
|
||||
"deleted_at": null,
|
||||
"created_by": "u_01a1787fef810090e8c864240d",
|
||||
"created_at": "2026-10-26T13:58:48.527Z",
|
||||
"updated_at": "2026-10-26T13:58:48.533Z",
|
||||
"created_by": "u_01a178865b9219bc0553c703f8",
|
||||
"created_at": "2026-10-26T14:05:48.980Z",
|
||||
"updated_at": "2026-10-26T14:05:48.990Z",
|
||||
"runtime": "static",
|
||||
"consent_version": null,
|
||||
"pending_cleanup_json": null,
|
||||
@@ -2286,8 +2286,8 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"token": "[redacted]",
|
||||
"install": "curl -fsSL http://127.0.0.1:41117/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:41117 --token … --external --site s_01a17881b3cfe6725e494551cd",
|
||||
"expiresAt": "2026-10-26T14:08:48.809Z",
|
||||
"install": "curl -fsSL http://127.0.0.1:45603/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:45603 --token … --external --site s_01a178881e3458f3e1da02398f",
|
||||
"expiresAt": "2026-10-26T14:15:49.217Z",
|
||||
"status": "pending"
|
||||
}
|
||||
},
|
||||
@@ -2483,8 +2483,8 @@
|
||||
"overall": "partial",
|
||||
"children": [
|
||||
"srv_missing_e2e:failed:서버가 없습니다",
|
||||
"srv_01a1787fe7fd6b1a739fe34322:done:",
|
||||
"srv_01a1787fe74fe122af4bf99ed5:done:"
|
||||
"srv_01a17886555747e79cb48c2573:done:",
|
||||
"srv_01a1788654a32b53cee3e1c344:done:"
|
||||
],
|
||||
"timedOut": false
|
||||
}
|
||||
@@ -2522,8 +2522,8 @@
|
||||
"overall": "partial",
|
||||
"children": [
|
||||
"srv_missing_e2e:failed:서버가 없습니다",
|
||||
"srv_01a1787fe7fd6b1a739fe34322:done:",
|
||||
"srv_01a1787fe74fe122af4bf99ed5:done:"
|
||||
"srv_01a17886555747e79cb48c2573:done:",
|
||||
"srv_01a1788654a32b53cee3e1c344:done:"
|
||||
],
|
||||
"timedOut": false
|
||||
},
|
||||
@@ -2562,7 +2562,7 @@
|
||||
"name": "critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회)",
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"id": "em_01a17881f90cb0a5ada262c099",
|
||||
"id": "em_01a1788860d3631819b57debc6",
|
||||
"noCred": "approvals_insufficient",
|
||||
"wrongStep": "wrong_step",
|
||||
"badIds": "identifiers_mismatch",
|
||||
@@ -2570,7 +2570,7 @@
|
||||
"approved": "approved",
|
||||
"approvals": 3,
|
||||
"credential": "[redacted]",
|
||||
"expiresAt": "2026-10-26T14:29:06.280Z"
|
||||
"expiresAt": "2026-10-26T14:36:06.064Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2591,14 +2591,14 @@
|
||||
"evidence": {
|
||||
"healthy": "hal_healthy",
|
||||
"sess": {
|
||||
"id": "em_01a17881f90cb0a5ada262c099",
|
||||
"id": "em_01a1788860d3631819b57debc6",
|
||||
"noCred": "approvals_insufficient",
|
||||
"wrongStep": "wrong_step",
|
||||
"badIds": "identifiers_mismatch",
|
||||
"badText": "final_confirm_mismatch",
|
||||
"approved": "approved",
|
||||
"approvals": 3,
|
||||
"expiresAt": "2026-10-26T14:29:06.280Z"
|
||||
"expiresAt": "2026-10-26T14:36:06.064Z"
|
||||
},
|
||||
"perf": {
|
||||
"forbidden": "action_forbidden",
|
||||
@@ -2650,8 +2650,8 @@
|
||||
"ok": true,
|
||||
"detail": {
|
||||
"status": "readonly",
|
||||
"deleteAfter": "2026-11-25T13:58:57.349Z",
|
||||
"readonlyUntil": "2026-11-25T13:58:57.349Z"
|
||||
"deleteAfter": "2026-11-25T14:05:55.878Z",
|
||||
"readonlyUntil": "2026-11-25T14:05:55.878Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2677,8 +2677,8 @@
|
||||
},
|
||||
"un": {
|
||||
"status": "readonly",
|
||||
"deleteAfter": "2026-11-25T13:58:57.349Z",
|
||||
"readonlyUntil": "2026-11-25T13:58:57.349Z"
|
||||
"deleteAfter": "2026-11-25T14:05:55.878Z",
|
||||
"readonlyUntil": "2026-11-25T14:05:55.878Z"
|
||||
},
|
||||
"sweep": {
|
||||
"sweep": 200,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 로컬 종단 간 검증 기록 — 2026-10-26
|
||||
|
||||
생성: 2026-10-26T13:59:08.928Z · `node --no-warnings test/e2e.test.mjs` (test/e2e/)
|
||||
생성: 2026-10-26T14:06:08.722Z · `node --no-warnings test/e2e.test.mjs` (test/e2e/)
|
||||
|
||||
실행 환경: 실제 HAL·고객 포털·Node Agent(명령 stub 모드) 프로세스 4대 + 가짜 Webmaker/AriMail/발신 메일 서버(127.0.0.1). 실 DNS·실 메일 왕복·실 서버는 포함하지 않는다.
|
||||
|
||||
@@ -88,20 +88,20 @@
|
||||
|
||||
### 2. 개인 사용자 그룹 생성과 다른 사용자 초대
|
||||
|
||||
- ✔ 팀 그룹 만들기 → 현재 그룹 전환 — g_01a1787ff02514257de360368a
|
||||
- ✔ 이메일 초대(7일) → 초대 메일 링크 — kL-VA6kLTcY9-9NNNN6LxXpXqWNXwskg
|
||||
- ✔ 팀 그룹 만들기 → 현재 그룹 전환 — g_01a178865c385db5f6ac357767
|
||||
- ✔ 이메일 초대(7일) → 초대 메일 링크 — YM3hdbeCEOx2z_Gg4jmKkLWTdZrFJ2PH
|
||||
- ✔ 초대 수락(1회용) → 별칭 user-02 · 역할 member — {"alias":"user-02","role":"member","reuse":"invite_invalid"}
|
||||
- ✔ 권한 없는 구성원의 직접 API 접근 → 403 + 필요한 역할 — {"requiredRole":["owner","admin"],"yourRole":"member"}
|
||||
- ✔ 그룹 전환 격리: 개인 그룹으로 바꾸면 팀 데이터가 보이지 않음 — {"team":{"live":0,"purchased":0,"inUse":0,"reserved":0,"unallocated":0},"personal":{"live":0,"purchased":0,"inUse":0,"reserved":0,"unallocated":0}}
|
||||
|
||||
### 3. 상품 주문, 결제 확인, 권리 활성화
|
||||
|
||||
- ✔ 상품 목록(공개 버전만·가격·한도·미확정 정책 표시) — {"count":2,"team":{"id":"pv_01a1787fed98a2869a7e65ec01","price":49000,"sites":3},"deferred":3,"paymentMethods":["manual_transfer"]}
|
||||
- ✔ 주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음 — {"orderNo":"AW-20261026-0001","status":"awaiting_payment","total":63800,"dueAt":"2026-11-02T13:56:53.016Z","hasInstructions":true}
|
||||
- ✔ 상품 목록(공개 버전만·가격·한도·미확정 정책 표시) — {"count":2,"team":{"id":"pv_01a178865ace62768069867677","price":49000,"sites":3},"deferred":3,"paymentMethods":["manual_transfer"]}
|
||||
- ✔ 주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음 — {"orderNo":"AW-20261026-0001","status":"awaiting_payment","total":63800,"dueAt":"2026-11-02T14:03:53.905Z","hasInstructions":true}
|
||||
- ✔ 입금 확인 전 승인 거부(409 payment_not_confirmed) — payment_not_confirmed
|
||||
- ✔ 운영자 입금 확인 → 승인 대기 목록에 권리 변화·현재 사용량 표시 — {"status":"awaiting_approval","delta":["sites","web_disk","web_out","mail_storage","mail_msgs","mailboxes","mail_services","mail_storage","mail_msgs","mailboxes"]}
|
||||
- ✔ 운영자 승인 → 권리 활성화(재승인은 중복 없음) — {"granted":10,"already":true}
|
||||
- ✔ 고객 결제 화면: 활성 상품·권리·다음 청구일 — {"sites":3,"subs":1,"nextBilling":"2026-11-26T13:56:53.082Z"}
|
||||
- ✔ 고객 결제 화면: 활성 상품·권리·다음 청구일 — {"sites":3,"subs":1,"nextBilling":"2026-11-26T14:03:53.969Z"}
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -112,25 +112,25 @@
|
||||
"bill": {
|
||||
"sites": 3,
|
||||
"subs": 1,
|
||||
"nextBilling": "2026-11-26T13:56:53.082Z"
|
||||
"nextBilling": "2026-11-26T14:03:53.969Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 초안 사이트 생성
|
||||
|
||||
- ✔ 사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내 — {"id":"s_01a1787ff118784dbb14dd9328","lifecycle":"draft","projectId":"00000000000000000000000000000001","nextSteps":["build","import","domain","mode","perms"]}
|
||||
- ✔ 사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내 — {"id":"s_01a178865d31eb6cc616c911ab","lifecycle":"draft","projectId":"00000000000000000000000000000001","nextSteps":["build","import","domain","mode","perms"]}
|
||||
- ✔ 사이트 목록 필터(초안)·사용량 요약에 예약 0 — {"drafts":1,"sitesReserved":0,"sitesAllocated":0}
|
||||
|
||||
### 5. Webmaker에서 페이지 제작
|
||||
|
||||
- ✔ 제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용) — {"portalRedirect":"http://127.0.0.1:46747/api/auth/sso?ticket=…","webmaker":302,"reuse":401}
|
||||
- ✔ 제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용) — {"portalRedirect":"http://127.0.0.1:33489/api/auth/sso?ticket=…","webmaker":302,"reuse":401}
|
||||
- ✔ Webmaker 에서 페이지 저장(저장본 2개) → 포털 저장본 목록·배포 전 확인 통과 — {"versions":1,"precheckOk":true}
|
||||
|
||||
```json
|
||||
{
|
||||
"sso": {
|
||||
"portalRedirect": "http://127.0.0.1:46747/api/auth/sso?ticket=…",
|
||||
"portalRedirect": "http://127.0.0.1:33489/api/auth/sso?ticket=…",
|
||||
"webmaker": 302,
|
||||
"reuse": 401
|
||||
},
|
||||
@@ -143,14 +143,14 @@
|
||||
|
||||
### 6. 관리형 배포
|
||||
|
||||
- ✔ 배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나 — {"jobId":"j_01a1787ff15708325bb3f4f76a","steps":10,"again":false,"siteCount":"reserved","server":null}
|
||||
- ✔ 배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나 — {"jobId":"j_01a178865d7c302bfebc85692e","steps":10,"again":false,"siteCount":"reserved","server":null}
|
||||
- ✔ HAL·Agent 를 거쳐 작업 완료 대기(최대 40초) — {"status":"done","steps":["accept:done","prepare_version:done","prepare_target:done","apply:done","data_check:done","start:done","internal_check:done","external_check:done","domain_https:done","done:done"]}
|
||||
- ✔ 배포 뒤 사이트 상태·버전·건강 — {"lifecycle":"needs_domain","version":"20261026135653-rev-c1-000000","verify":"needs_domain","health":"ok","agentHasSite":true}
|
||||
- ✔ 배포 뒤 사이트 상태·버전·건강 — {"lifecycle":"needs_domain","version":"20261026140355-rev-c1-000000","verify":"needs_domain","health":"ok","agentHasSite":true}
|
||||
|
||||
```json
|
||||
{
|
||||
"d": {
|
||||
"jobId": "j_01a1787ff15708325bb3f4f76a",
|
||||
"jobId": "j_01a178865d7c302bfebc85692e",
|
||||
"steps": 10,
|
||||
"again": false,
|
||||
"siteCount": "reserved",
|
||||
@@ -173,7 +173,7 @@
|
||||
},
|
||||
"after": {
|
||||
"lifecycle": "needs_domain",
|
||||
"version": "20261026135653-rev-c1-000000",
|
||||
"version": "20261026140355-rev-c1-000000",
|
||||
"verify": "needs_domain",
|
||||
"health": "ok",
|
||||
"agentHasSite": true
|
||||
@@ -183,14 +183,14 @@
|
||||
|
||||
### 7. 도메인 연결과 HTTPS 발급
|
||||
|
||||
- ✔ 도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT) — {"domainId":"d_01a17880012bc25aa36fca86f9","txtName":"_ariweb-verify.aw-e2e.localhost","txtValueShape":"ariweb-verify=…","copyable":true}
|
||||
- ✔ 도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT) — {"domainId":"d_01a178866d50f0bd814ff16339","txtName":"_ariweb-verify.aw-e2e.localhost","txtValueShape":"ariweb-verify=…","copyable":true}
|
||||
- ✔ 도메인 목록·상세: 소유 확인·DNS·인증서 상태 표시 — {}
|
||||
- ✔ (정리) 도메인 연결 작업 취소 — 포털이 외부 대기 중에도 5초마다 HAL domain.probe 를 새로 만들어 큐를 채우므로(결함 항목) 뒤 흐름을 위해 멈춘다 — {"queuedProbes":0}
|
||||
|
||||
```json
|
||||
{
|
||||
"dm": {
|
||||
"domainId": "d_01a17880012bc25aa36fca86f9",
|
||||
"domainId": "d_01a178866d50f0bd814ff16339",
|
||||
"txtName": "_ariweb-verify.aw-e2e.localhost",
|
||||
"txtValueShape": "ariweb-verify=…",
|
||||
"copyable": true
|
||||
@@ -233,9 +233,9 @@
|
||||
|
||||
### 10. 사이트 대표 메일과 메일함 생성
|
||||
|
||||
- ✔ 사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시 — {"id":"msv_01a1788089b2517001dca93c90","status":"needs_setup","tenant":true,"ariwebKind":"site_bound","limitAccounts":6,"siteCountAllocated":0}
|
||||
- ✔ 사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시 — {"id":"msv_01a17886f5eea0a3261b10719a","status":"needs_setup","tenant":true,"ariwebKind":"site_bound","limitAccounts":6,"siteCountAllocated":0}
|
||||
- ✔ 같은 사이트에 두 번째 대표 메일 거부 — mail_domain_in_use
|
||||
- ✔ 메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음 — {"id":"mb_01a1788089ea344ac20fff0cff","address":"kim@aw-e2e.localhost","passwordShownOnce":"[redacted]","mustChange":true,"imapHost":"127.0.0.1","leakedOnReload":false,"inArimail":true}
|
||||
- ✔ 메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음 — {"id":"mb_01a17886f6282d11bf30593b13","address":"kim@aw-e2e.localhost","passwordShownOnce":"[redacted]","mustChange":true,"imapHost":"127.0.0.1","leakedOnReload":false,"inArimail":true}
|
||||
- ✔ 초대 방식 메일함(링크 만료·재발송·사용 완료 상태) — {"inviteState":"pending","accepted":200,"reuse":"invite_used"}
|
||||
- ✔ DNS 확인: 미반영/잘못된 값 구분 표시(로컬 DNS 에는 레코드 없음 → missing) — {"ok":false,"missing":3,"items":["mx:missing","spf:missing","dkim:missing","dmarc:recommended"]}
|
||||
- ✔ 활성화 작업 → DNS 외부 대기(awaiting_external) · 멱등 — {"status":"awaiting_external","waitReason":"DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요","sameJob":true,"steps":["entitlement:done","dns:waiting","prepare:pending","verify:pending","activate:pending"]}
|
||||
@@ -244,7 +244,7 @@
|
||||
```json
|
||||
{
|
||||
"svc": {
|
||||
"id": "msv_01a1788089b2517001dca93c90",
|
||||
"id": "msv_01a17886f5eea0a3261b10719a",
|
||||
"status": "needs_setup",
|
||||
"tenant": true,
|
||||
"ariwebKind": "site_bound",
|
||||
@@ -252,7 +252,7 @@
|
||||
"siteCountAllocated": 0
|
||||
},
|
||||
"mb": {
|
||||
"id": "mb_01a1788089ea344ac20fff0cff",
|
||||
"id": "mb_01a17886f6282d11bf30593b13",
|
||||
"address": "kim@aw-e2e.localhost",
|
||||
"passwordShownOnce": "[redacted]",
|
||||
"mustChange": true,
|
||||
@@ -287,13 +287,13 @@
|
||||
|
||||
### 12. 독립 메일 서비스 생성
|
||||
|
||||
- ✔ 메일 전용 도메인 등록(solo-e2e.localhost, purpose mail) — {"id":"d_01a17880affa55291738680a3a","jobCancelled":null}
|
||||
- ✔ 독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비) — {"id":"msv_01a17880b003f52685dc3a1e83","kind":"standalone","mailServicesAllocated":1,"sitesAllocated":0}
|
||||
- ✔ 메일 전용 도메인 등록(solo-e2e.localhost, purpose mail) — {"id":"d_01a17887203cc05d2e6538d60b","jobCancelled":null}
|
||||
- ✔ 독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비) — {"id":"msv_01a1788720453f9268e2f9c599","kind":"standalone","mailServicesAllocated":1,"sitesAllocated":0}
|
||||
|
||||
```json
|
||||
{
|
||||
"r": {
|
||||
"id": "msv_01a17880b003f52685dc3a1e83",
|
||||
"id": "msv_01a1788720453f9268e2f9c599",
|
||||
"kind": "standalone",
|
||||
"mailServicesAllocated": 1,
|
||||
"sitesAllocated": 0
|
||||
@@ -307,8 +307,8 @@
|
||||
|
||||
- ✔ 백업 옵션 주문(대상 사이트)·입금 확인·승인 → backup 권리 — {"granted":7,"orderStatus":"approved"}
|
||||
- ✔ 백업 화면: 구매 가능 여부·저장 한도·일정·보존·첫 백업 기한·검증 방식(상품정책에서) — {"eligible":true,"reasons":[],"deferred":[],"policy":null,"status":200}
|
||||
- ✔ 옵션 활성화(확인 필수) → 첫 백업 예정 시각 — {"status":"active","firstDue":"2026-10-27T13:57:42.172Z","nextRun":"2026-10-26T13:57:42.172Z"}
|
||||
- ✔ 지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증 — {"timedOut":false,"optionStatus":"active","lastJob":null,"siteServer":"srv_01a1787fe74fe122af4bf99ed5","backup":{"status":"done","tier":"monthly","location":null},"job":null,"optionFailures":0}
|
||||
- ✔ 옵션 활성화(확인 필수) → 첫 백업 예정 시각 — {"status":"active","firstDue":"2026-10-27T14:04:44.126Z","nextRun":"2026-10-26T14:04:44.126Z"}
|
||||
- ✔ 지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증 — {"timedOut":false,"optionStatus":"active","lastJob":null,"siteServer":"srv_01a1788654a32b53cee3e1c344","backup":{"status":"done","tier":"monthly","location":null},"job":null,"optionFailures":0}
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -321,14 +321,14 @@
|
||||
},
|
||||
"opt": {
|
||||
"status": "active",
|
||||
"firstDue": "2026-10-27T13:57:42.172Z",
|
||||
"nextRun": "2026-10-26T13:57:42.172Z"
|
||||
"firstDue": "2026-10-27T14:04:44.126Z",
|
||||
"nextRun": "2026-10-26T14:04:44.126Z"
|
||||
},
|
||||
"run1": {
|
||||
"timedOut": false,
|
||||
"optionStatus": "active",
|
||||
"lastJob": null,
|
||||
"siteServer": "srv_01a1787fe74fe122af4bf99ed5",
|
||||
"siteServer": "srv_01a1788654a32b53cee3e1c344",
|
||||
"backup": {
|
||||
"status": "done",
|
||||
"tier": "monthly",
|
||||
@@ -342,8 +342,8 @@
|
||||
|
||||
### 14. 백업 복원과 외부 사이트 확인
|
||||
|
||||
- ✔ 복원 안내(포함 데이터·영향·보호 시점 유지·되돌릴 수 있는 마지막 시각·완료 방식) — {"backup":{"id":"bk_01a17880c4e1bdfcb12b9e2585","createdAt":"2026-10-26T13:57:42.179Z","scope":{"files":true,"db":true,"uploads":true,"config":true,"storeServerId":"srv_01a1787fe7fd6b1a739fe34322","verifyMethod":"sha256 + 격리 환경 읽기 확인"},"sizeBytes":1907,"verifyState":"verified","expiresAt":"2027-01-2
|
||||
- ✔ 복원 요청 → 운영자 승인 대기 · 중복 요청 없음 — {"id":"rs_01a17880c8586bb0433c63ec1d","status":"awaiting_approval","dup":false,"finalConfirmation":{"group":"E2E 팀","site":"E2E 사이트","domain":null}}
|
||||
- ✔ 복원 안내(포함 데이터·영향·보호 시점 유지·되돌릴 수 있는 마지막 시각·완료 방식) — {"backup":{"id":"bk_01a178873211d14d742b283b95","createdAt":"2026-10-26T14:04:44.134Z","scope":{"files":true,"db":true,"uploads":true,"config":true,"storeServerId":"srv_01a17886555747e79cb48c2573","verifyMethod":"sha256 + 격리 환경 읽기 확인"},"sizeBytes":1895,"verifyState":"verified","expiresAt":"2027-01-2
|
||||
- ✔ 복원 요청 → 운영자 승인 대기 · 중복 요청 없음 — {"id":"rs_01a1788732b6ab8b3f85488a93","status":"awaiting_approval","dup":false,"finalConfirmation":{"group":"E2E 팀","site":"E2E 사이트","domain":null}}
|
||||
- ✔ 운영자 최종 확인(그룹·사이트·백업 시각) 불일치 거부 → 일치 승인(멱등) — {"bad":"final_confirmation_mismatch","ok":400}
|
||||
- ✔ 9단계 작업 진행 → 사용자 확인 필요(보호 시점·되돌리기 가능) — {"steps":[]}
|
||||
|
||||
@@ -351,19 +351,19 @@
|
||||
{
|
||||
"info": {
|
||||
"backup": {
|
||||
"id": "bk_01a17880c4e1bdfcb12b9e2585",
|
||||
"createdAt": "2026-10-26T13:57:42.179Z",
|
||||
"id": "bk_01a178873211d14d742b283b95",
|
||||
"createdAt": "2026-10-26T14:04:44.134Z",
|
||||
"scope": {
|
||||
"files": true,
|
||||
"db": true,
|
||||
"uploads": true,
|
||||
"config": true,
|
||||
"storeServerId": "srv_01a1787fe7fd6b1a739fe34322",
|
||||
"storeServerId": "srv_01a17886555747e79cb48c2573",
|
||||
"verifyMethod": "sha256 + 격리 환경 읽기 확인"
|
||||
},
|
||||
"sizeBytes": 1907,
|
||||
"sizeBytes": 1895,
|
||||
"verifyState": "verified",
|
||||
"expiresAt": "2027-01-27T13:57:47.362Z",
|
||||
"expiresAt": "2027-01-27T14:04:48.529Z",
|
||||
"tier": "monthly"
|
||||
},
|
||||
"included": [
|
||||
@@ -393,7 +393,7 @@
|
||||
"deferred": []
|
||||
},
|
||||
"rq": {
|
||||
"id": "rs_01a17880c8586bb0433c63ec1d",
|
||||
"id": "rs_01a1788732b6ab8b3f85488a93",
|
||||
"status": "awaiting_approval",
|
||||
"dup": false,
|
||||
"finalConfirmation": {
|
||||
@@ -445,10 +445,10 @@
|
||||
|
||||
### 16. 외부 배포 사이트 생성과 배포
|
||||
|
||||
- ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a17881b3cfe6725e494551cd","group_id":"g_01a1787ff02514257de360368a","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou
|
||||
- ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a178881e3458f3e1da02398f","group_id":"g_01a178865c385db5f6ac357767","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou
|
||||
- ✔ 배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전 — {"notProvided":"[redacted]","consentVersion":"external-deploy-2026-10-0.1.1","agreed":false}
|
||||
- ✔ 동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전) — {"before":"consent_required","consent":200}
|
||||
- ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:41117/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:41117 --token … --external --site s_01a17881b3cfe6725e494551cd","expiresAt":"2026-10-26T14:08:48.809Z","status":"pending"}
|
||||
- ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:45603/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:45603 --token … --external --site s_01a178881e3458f3e1da02398f","expiresAt":"2026-10-26T14:15:49.217Z","status":"pending"}
|
||||
- ✔ 고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속) — {"agentId":"[redacted]"}
|
||||
- ✔ 포털 연결 확인(외부 Agent 상태 connected) — {"status":"connected","stale":false,"halAgentId":"[redacted]"}
|
||||
- ✔ 배포 전 확인(외부: 관리형 용량 확인 없음) — {"ok":true,"blocks":[]}
|
||||
@@ -465,8 +465,8 @@
|
||||
"agreed": false
|
||||
},
|
||||
"tok": {
|
||||
"install": "curl -fsSL http://127.0.0.1:41117/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:41117 --token … --external --site s_01a17881b3cfe6725e494551cd",
|
||||
"expiresAt": "2026-10-26T14:08:48.809Z"
|
||||
"install": "curl -fsSL http://127.0.0.1:45603/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:45603 --token … --external --site s_01a178881e3458f3e1da02398f",
|
||||
"expiresAt": "2026-10-26T14:15:49.217Z"
|
||||
},
|
||||
"conn": {
|
||||
"status": "connected",
|
||||
@@ -554,7 +554,7 @@
|
||||
- ✔ 축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시 — {"shortages":["mailboxes:1"]}
|
||||
- ✔ 축소 요청 → 보류(사용 중 권리 즉시 회수 없음) — {"fits":false,"hold":"holding","sitesStillPurchased":3}
|
||||
- ✔ 정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성 — {"allocate":200,"fits":true,"shortages":[],"order":"awaiting_payment"}
|
||||
- ✔ 축소 주문 입금 확인·승인 → 권리 교체(베이직) — {"approve":200,"sites":1,"mailboxes":5,"product":"pv_01a1787fed98b01207eddba3f9"}
|
||||
- ✔ 축소 주문 입금 확인·승인 → 권리 교체(베이직) — {"approve":200,"sites":1,"mailboxes":5,"product":"pv_01a178865acd6d92fafdc54f8f"}
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -589,7 +589,7 @@
|
||||
"approve": 200,
|
||||
"sites": 1,
|
||||
"mailboxes": 5,
|
||||
"product": "pv_01a1787fed98b01207eddba3f9"
|
||||
"product": "pv_01a178865acd6d92fafdc54f8f"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -623,10 +623,10 @@
|
||||
### 20. 사이트 소유권 이전
|
||||
|
||||
- ✔ 이전 범위 미리보기(사이트·도메인·대표 메일·배정·진행 작업·미납 / 제외 항목) — {"sites":1,"domains":1,"siteMail":1,"needed":{"backup":1,"web_disk":1073741824,"mail_msgs":500,"mail_storage":2147483648,"mailboxes":6},"excluded":3}
|
||||
- ✔ 요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림 — {"id":"tr_01a17881d680c702999f932704","status":"requested","expiresAt":"2026-11-02T13:58:57.408Z","bobNotified":true}
|
||||
- ✔ 요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림 — {"id":"tr_01a178883968273245f1a10b7c","status":"requested","expiresAt":"2026-11-02T14:05:55.944Z","bobNotified":true}
|
||||
- ✔ (정리) 받는 쪽 권리에 맞게 범위 조정: 백업 옵션 종료·메일함 배정 6→5(받는 그룹 베이직 5개) — {"endBackup":200,"alloc":200}
|
||||
- ✔ 받는 그룹 수락 → 권리 부족이면 보류(필요한 상품 안내) — {"status":200,"tstatus":"reviewing","hold":null}
|
||||
- ✔ 운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각 — {"bad":"final_confirmation_mismatch","ok":200,"status":"completed","cutover":"2026-10-26T13:58:57.455Z"}
|
||||
- ✔ 운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각 — {"bad":"final_confirmation_mismatch","ok":200,"status":"completed","cutover":"2026-10-26T14:05:55.994Z"}
|
||||
- ✔ 완료 뒤: 보내는 쪽은 접근 불가(404), 받는 쪽 대시보드·사용량 일치, 불변 감사 — {"aliceSees":404,"bobSees":200,"bobUsageSites":0,"audit":true}
|
||||
|
||||
```json
|
||||
@@ -645,9 +645,9 @@
|
||||
"excluded": 3
|
||||
},
|
||||
"rq": {
|
||||
"id": "tr_01a17881d680c702999f932704",
|
||||
"id": "tr_01a178883968273245f1a10b7c",
|
||||
"status": "requested",
|
||||
"expiresAt": "2026-11-02T13:58:57.408Z",
|
||||
"expiresAt": "2026-11-02T14:05:55.944Z",
|
||||
"bobNotified": true
|
||||
},
|
||||
"acc": {
|
||||
@@ -659,7 +659,7 @@
|
||||
"bad": "final_confirmation_mismatch",
|
||||
"ok": 200,
|
||||
"status": "completed",
|
||||
"cutover": "2026-10-26T13:58:57.455Z"
|
||||
"cutover": "2026-10-26T14:05:55.994Z"
|
||||
},
|
||||
"after": {
|
||||
"aliceSees": 404,
|
||||
@@ -798,7 +798,7 @@
|
||||
|
||||
### 23. 실패 작업의 재시도와 되돌리기
|
||||
|
||||
- ✔ 배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호 — {"status":"done","canRetry":false,"jobId":"j_01a1787ff15708325bb3f4f76a"}
|
||||
- ✔ 배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호 — {"status":"done","canRetry":false,"jobId":"j_01a178865d7c302bfebc85692e"}
|
||||
- ✔ 재시도 요청(POST /api/sites/:id/jobs/:jobId/retry) — {"status":400,"error":"job_not_retryable"}
|
||||
- ✔ 되돌리기 요청(POST /api/sites/:id/rollback) — 이전 버전 없으면 거부 — {"status":409,"error":"no_previous","message":"되돌릴 이전 버전이 없습니다"}
|
||||
|
||||
@@ -807,7 +807,7 @@
|
||||
"dep": {
|
||||
"status": "done",
|
||||
"canRetry": false,
|
||||
"jobId": "j_01a1787ff15708325bb3f4f76a"
|
||||
"jobId": "j_01a178865d7c302bfebc85692e"
|
||||
},
|
||||
"rt": {
|
||||
"status": 400,
|
||||
@@ -823,16 +823,16 @@
|
||||
|
||||
### 24. 운영자 승인, 감사 기록, 고객 알림의 일치
|
||||
|
||||
- ✔ 고객 알림 센터에 승인 알림 — {"title":"주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다","category":"billing","link":"/orders/o_01a1787ff097f1475baf324276"}
|
||||
- ✔ 고객 알림 센터에 승인 알림 — {"title":"주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다","category":"billing","link":"/orders/o_01a178865cb1f3d04a833a14fa"}
|
||||
- ✔ 감사 기록: order.approve 는 삭제 불가(immutable) — {"action":"order.approve","immutable":true}
|
||||
- ✔ 운영자 화면과 고객 주문 화면의 상태 일치 — {"status":"approved","approvedAt":"2026-10-26T13:56:53.082Z"}
|
||||
- ✔ 운영자 화면과 고객 주문 화면의 상태 일치 — {"status":"approved","approvedAt":"2026-10-26T14:03:53.969Z"}
|
||||
|
||||
```json
|
||||
{
|
||||
"n": {
|
||||
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
|
||||
"category": "billing",
|
||||
"link": "/orders/o_01a1787ff097f1475baf324276"
|
||||
"link": "/orders/o_01a178865cb1f3d04a833a14fa"
|
||||
},
|
||||
"a": {
|
||||
"action": "order.approve",
|
||||
@@ -840,7 +840,7 @@
|
||||
},
|
||||
"same": {
|
||||
"status": "approved",
|
||||
"approvedAt": "2026-10-26T13:56:53.082Z"
|
||||
"approvedAt": "2026-10-26T14:03:53.969Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -848,7 +848,7 @@
|
||||
### 25. HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련
|
||||
|
||||
- ✔ 설정 전 준비 상태(미설정 항목) — {"ready":false,"missingConfig":["rto_minutes","rpo_minutes","snapshot_interval_minutes","retention_count"],"lastDrill":null,"lastSnapshot":null,"config":{"rto_minutes":null,"rpo_minutes":null,"snapshot_interval_minutes":null,"retention_count":null,"external_store":null},"label":"정식 운영 준비 전(복구 훈련 또는
|
||||
- ✔ RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정 — {"id":1,"rto_minutes":60,"rpo_minutes":30,"snapshot_interval_minutes":60,"retention_count":3,"external_store":"/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026135648/dr-external","updated_at":"2026-10-26T13:59:05.848Z","updated_by":"op_01a1787fe64f62e76ecfed50f6"}
|
||||
- ✔ RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정 — {"id":1,"rto_minutes":60,"rpo_minutes":30,"snapshot_interval_minutes":60,"retention_count":3,"external_store":"/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026140349/dr-external","updated_at":"2026-10-26T14:06:05.749Z","updated_by":"op_01a1788653a9a3257ffd73d6d8"}
|
||||
- ✔ 복구 사본 생성·검증·외부 보관(vault.key 제외) — {"verify":"ok","external":true,"sha":true}
|
||||
- ✔ 복원 요청 → 승인 전 시작 거부 → 승인 → 기존 제어 중지 확인 없이는 거부 → 시작(명령 안내) → 확인 항목 부족 거부 → 완료 — {"early":"restore_not_approved","noStop":"old_control_not_confirmed","command":true,"incomplete":"checks_incomplete","final":"completed"}
|
||||
- ✔ 복구 훈련: 항목 하나 실패 → failed, 모두 성공·측정 RTO/RPO → 준비 완료 — {"first":"failed","second":"success","ready":true,"measured":{}}
|
||||
@@ -880,9 +880,9 @@
|
||||
"rpo_minutes": 30,
|
||||
"snapshot_interval_minutes": 60,
|
||||
"retention_count": 3,
|
||||
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026135648/dr-external",
|
||||
"updated_at": "2026-10-26T13:59:05.848Z",
|
||||
"updated_by": "op_01a1787fe64f62e76ecfed50f6"
|
||||
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026140349/dr-external",
|
||||
"updated_at": "2026-10-26T14:06:05.749Z",
|
||||
"updated_by": "op_01a1788653a9a3257ffd73d6d8"
|
||||
},
|
||||
"snap": {
|
||||
"verify": "ok",
|
||||
@@ -907,15 +907,15 @@
|
||||
|
||||
### 26. 새로 고침, 재로그인, HAL 재시작 후 진행 작업 상태 유지
|
||||
|
||||
- ✔ 재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록 — {"mailJob":{"id":"j_01a178809a29be75e2c1ee2f1b","status":"awaiting_external"},"halJobs":18}
|
||||
- ✔ 포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지 — {"sessionKept":"[redacted]","mailJob":{"id":"j_01a178809a29be75e2c1ee2f1b","status":"awaiting_external"},"same":true}
|
||||
- ✔ 재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록 — {"mailJob":{"id":"j_01a178870a63a734460ce886f3","status":"awaiting_external"},"halJobs":18}
|
||||
- ✔ 포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지 — {"sessionKept":"[redacted]","mailJob":{"id":"j_01a178870a63a734460ce886f3","status":"awaiting_external"},"same":true}
|
||||
- ✔ HAL 재시작 → 운영자 세션·작업 목록·서버 등록 유지 — {"opsSessionKept":"[redacted]","halJobs":18,"sameCount":true}
|
||||
|
||||
```json
|
||||
{
|
||||
"before": {
|
||||
"mailJob": {
|
||||
"id": "j_01a178809a29be75e2c1ee2f1b",
|
||||
"id": "j_01a178870a63a734460ce886f3",
|
||||
"status": "awaiting_external"
|
||||
},
|
||||
"halJobs": 18
|
||||
@@ -923,7 +923,7 @@
|
||||
"pr": {
|
||||
"sessionKept": "[redacted]",
|
||||
"mailJob": {
|
||||
"id": "j_01a178809a29be75e2c1ee2f1b",
|
||||
"id": "j_01a178870a63a734460ce886f3",
|
||||
"status": "awaiting_external"
|
||||
},
|
||||
"same": true
|
||||
@@ -939,7 +939,7 @@
|
||||
### 27. 마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공
|
||||
|
||||
- ✔ 밥 개인 그룹: 베이직(사이트 1) 주문·승인 — {"granted":6}
|
||||
- ✔ 사이트 2개 생성·저장본 — [{"id":"s_01a178807db2350aa3520b04a8","group_id":"g_01a1787ff051603dc8fd16c945","name":"동시 A","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000002","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"managed","primary_domain":null,"site_count_sta
|
||||
- ✔ 사이트 2개 생성·저장본 — [{"id":"s_01a17886e9ee4810dcf115f705","group_id":"g_01a178865c69eac738e0dbf93e","name":"동시 A","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000002","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"managed","primary_domain":null,"site_count_sta
|
||||
- ✔ 두 사이트 동시 최초 배포 요청 → 하나만 200, 다른 쪽 409 precheck_failed(sites 부족) — {"ok":1,"blocked":"precheck_failed","shortage":"block"}
|
||||
- ✔ (정리) 선행 결함으로 영원히 진행 중인 배포 취소·예약 반환 — {"cancel":200,"cleanup":200,"unallocated":0}
|
||||
|
||||
@@ -955,10 +955,10 @@
|
||||
|
||||
### 28. 외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리
|
||||
|
||||
- ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a17881b3cfe6725e494551cd","group_id":"g_01a1787ff02514257de360368a","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou
|
||||
- ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a178881e3458f3e1da02398f","group_id":"g_01a178865c385db5f6ac357767","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou
|
||||
- ✔ 배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전 — {"notProvided":"[redacted]","consentVersion":"external-deploy-2026-10-0.1.1","agreed":false}
|
||||
- ✔ 동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전) — {"before":"consent_required","consent":200}
|
||||
- ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:41117/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:41117 --token … --external --site s_01a17881b3cfe6725e494551cd","expiresAt":"2026-10-26T14:08:48.809Z","status":"pending"}
|
||||
- ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:45603/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:45603 --token … --external --site s_01a178881e3458f3e1da02398f","expiresAt":"2026-10-26T14:15:49.217Z","status":"pending"}
|
||||
- ✔ 고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속) — {"agentId":"[redacted]"}
|
||||
- ✔ 포털 연결 확인(외부 Agent 상태 connected) — {"status":"connected","stale":false,"halAgentId":"[redacted]"}
|
||||
- ✔ 배포 전 확인(외부: 관리형 용량 확인 없음) — {"ok":true,"blocks":[]}
|
||||
@@ -1039,7 +1039,7 @@
|
||||
|
||||
### 30. 모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정
|
||||
|
||||
- ✔ agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed) — {"status":"failed","errorCode":"partial_failure","overall":"partial","children":["srv_missing_e2e:failed:서버가 없습니다","srv_01a1787fe7fd6b1a739fe34322:done:","srv_01a1787fe74fe122af4bf99ed5:done:"],"timedOut":false}
|
||||
- ✔ agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed) — {"status":"failed","errorCode":"partial_failure","overall":"partial","children":["srv_missing_e2e:failed:서버가 없습니다","srv_01a17886555747e79cb48c2573:done:","srv_01a1788654a32b53cee3e1c344:done:"],"timedOut":false}
|
||||
- ✔ 모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업 — {"health":"done","drift":0,"reconcile":200,"overviewComponents":["HAL:ok","portal:unknown","webmaker:unknown","arimail:unknown","backup:ok","observability:ok"]}
|
||||
- ✔ 되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부 — {"step1":"confirm_impact_required","step2":"identifiers_mismatch"}
|
||||
|
||||
@@ -1051,8 +1051,8 @@
|
||||
"overall": "partial",
|
||||
"children": [
|
||||
"srv_missing_e2e:failed:서버가 없습니다",
|
||||
"srv_01a1787fe7fd6b1a739fe34322:done:",
|
||||
"srv_01a1787fe74fe122af4bf99ed5:done:"
|
||||
"srv_01a17886555747e79cb48c2573:done:",
|
||||
"srv_01a1788654a32b53cee3e1c344:done:"
|
||||
],
|
||||
"timedOut": false
|
||||
},
|
||||
@@ -1079,21 +1079,21 @@
|
||||
### 31. 긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구
|
||||
|
||||
- ✔ HAL 정상 상태에서 선언 거부(hal_healthy) — hal_healthy
|
||||
- ✔ critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회) — {"id":"em_01a17881f90cb0a5ada262c099","noCred":"approvals_insufficient","wrongStep":"wrong_step","badIds":"identifiers_mismatch","badText":"final_confirm_mismatch","approved":"approved","approvals":3,"credential":"[redacted]","expiresAt":"2026-10-26T14:29:06.280Z"}
|
||||
- ✔ critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회) — {"id":"em_01a1788860d3631819b57debc6","noCred":"approvals_insufficient","wrongStep":"wrong_step","badIds":"identifiers_mismatch","badText":"final_confirm_mismatch","approved":"approved","approvals":3,"credential":"[redacted]","expiresAt":"2026-10-26T14:36:06.064Z"}
|
||||
- ✔ 일반 배포(module.deploy) 거부 · 범위 밖 거부 · 잘못된 자격 거부 · 허용 행동 수행 · 확인 뒤 자격 폐기 · 폐기 뒤 거부 · 조정 · 종료 — {"forbidden":"action_forbidden","outOfScope":"out_of_scope","badCred":"bad_credential","performed":400,"revoked":true,"afterRevoke":"credential_revoked","reconcile":200,"closed":"closed"}
|
||||
|
||||
```json
|
||||
{
|
||||
"healthy": "hal_healthy",
|
||||
"sess": {
|
||||
"id": "em_01a17881f90cb0a5ada262c099",
|
||||
"id": "em_01a1788860d3631819b57debc6",
|
||||
"noCred": "approvals_insufficient",
|
||||
"wrongStep": "wrong_step",
|
||||
"badIds": "identifiers_mismatch",
|
||||
"badText": "final_confirm_mismatch",
|
||||
"approved": "approved",
|
||||
"approvals": 3,
|
||||
"expiresAt": "2026-10-26T14:29:06.280Z"
|
||||
"expiresAt": "2026-10-26T14:36:06.064Z"
|
||||
},
|
||||
"perf": {
|
||||
"forbidden": "action_forbidden",
|
||||
@@ -1113,7 +1113,7 @@
|
||||
- ✔ Source 옵션 주문(대상 사이트)·승인 → source 권리 — {"granted":7}
|
||||
- ✔ Source 옵션 활성화(권리 안에서) → 저장소·브랜치·변경 이력·clone 정보 — {"activate":200,"status":"active","provider":"gitea","changes":1,"clone":true}
|
||||
- ✔ export(저장본 내려받기) — {"status":200,"type":"application/gzip"}
|
||||
- ✔ 옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단 — {"status":"readonly","deleteAfter":"2026-11-25T13:58:57.349Z","readonlyUntil":"2026-11-25T13:58:57.349Z"}
|
||||
- ✔ 옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단 — {"status":"readonly","deleteAfter":"2026-11-25T14:05:55.878Z","readonlyUntil":"2026-11-25T14:05:55.878Z"}
|
||||
- ✔ 삭제 정리 작업(sweep)은 삭제 예정일 전이면 지우지 않음 — {"sweep":200,"statusAfter":"readonly"}
|
||||
|
||||
```json
|
||||
@@ -1131,8 +1131,8 @@
|
||||
},
|
||||
"un": {
|
||||
"status": "readonly",
|
||||
"deleteAfter": "2026-11-25T13:58:57.349Z",
|
||||
"readonlyUntil": "2026-11-25T13:58:57.349Z"
|
||||
"deleteAfter": "2026-11-25T14:05:55.878Z",
|
||||
"readonlyUntil": "2026-11-25T14:05:55.878Z"
|
||||
},
|
||||
"sweep": {
|
||||
"sweep": 200,
|
||||
|
||||
+15
-13
@@ -38,6 +38,8 @@ export function create(deps) {
|
||||
const list = await api.domains(s.id); const d = list.find((x) => x.domain === domain); const host = d ? String(d.hosts[0]).split('/')[0] : `mail.${domain}`;
|
||||
hostCache.set(s.id + '|' + domain, { host, at: Date.now() }); return host;
|
||||
}
|
||||
/** 전체 설정 API 호출: AriMail 은 기본 도메인 호스트(setup_host, 예: mail.arihakdang.com)에서만 설정 API 를 응답하므로 URL 은 setup_url(인증서 이름·내부 주소)로 두고 Host 헤더를 따로 보낸다. */
|
||||
const setupCall = (s, path, opts = {}) => call(fetchImpl, setupOf(s) + path, { ...opts, host: s.setup_host || opts.host || null, headers: { ...setupHeaders(s), ...(opts.headers || {}) } });
|
||||
const setupHeaders = (s) => { const tok = vault.get(secretName(s.id, 'setup-token')); if (!tok) { const e = new Error('AriMail 전체 설정 토큰이 보관소에 없습니다'); e.code = 'mail_server_token_missing'; throw e; } return { 'X-Setup-Token': tok }; };
|
||||
/** 관리자 세션 쿠키(도메인별). 보관소의 관리자 로그인으로 로그인해 10분 캐시. */
|
||||
const adminCache = new Map();
|
||||
@@ -57,12 +59,12 @@ export function create(deps) {
|
||||
defaultServer() { return serverRow(null); },
|
||||
getServer: (id) => serverRow(id),
|
||||
/** 서버 등록·수정. secrets 는 보관소에만 저장하고 응답에 돌려주지 않는다. */
|
||||
saveServer({ id = null, name, baseUrl, internalUrl = null, setupUrl = null, mailHost = null, isDefault = false, secrets = {} }) {
|
||||
saveServer({ id = null, name, baseUrl, internalUrl = null, setupUrl = null, setupHost = null, mailHost = null, isDefault = false, secrets = {} }) {
|
||||
const now = nowIso(); const sid = id || newId('ms_');
|
||||
if (!/^https?:\/\//.test(String(baseUrl || ''))) throw Object.assign(new Error('공개 주소는 http(s):// 로 시작해야 합니다'), { code: 'validation', status: 422 });
|
||||
if (isDefault) store.run('UPDATE mail_servers SET is_default=0');
|
||||
if (id) store.run('UPDATE mail_servers SET name=?, base_url=?, internal_url=?, setup_url=?, mail_host=?, is_default=?, updated_at=? WHERE id=?', name, baseUrl, internalUrl, setupUrl, mailHost, isDefault ? 1 : 0, now, id);
|
||||
else store.run('INSERT INTO mail_servers(id, name, base_url, internal_url, setup_url, mail_host, is_default, created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?)', sid, name, baseUrl, internalUrl, setupUrl, mailHost, isDefault || !store.get('SELECT 1 FROM mail_servers') ? 1 : 0, now, now);
|
||||
if (id) store.run('UPDATE mail_servers SET name=?, base_url=?, internal_url=?, setup_url=?, setup_host=?, mail_host=?, is_default=?, updated_at=? WHERE id=?', name, baseUrl, internalUrl, setupUrl, setupHost, mailHost, isDefault ? 1 : 0, now, id);
|
||||
else store.run('INSERT INTO mail_servers(id, name, base_url, internal_url, setup_url, setup_host, mail_host, is_default, created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)', sid, name, baseUrl, internalUrl, setupUrl, setupHost, mailHost, isDefault || !store.get('SELECT 1 FROM mail_servers') ? 1 : 0, now, now);
|
||||
for (const [k, v] of Object.entries(secrets)) if (v) vault.set(secretName(sid, k), typeof v === 'string' ? v : JSON.stringify(v), { kind: 'arimail' });
|
||||
return api.listServers().find((s) => s.id === sid);
|
||||
},
|
||||
@@ -80,25 +82,25 @@ export function create(deps) {
|
||||
return r.json;
|
||||
},
|
||||
// ---- 전체 설정 API(도메인 등록부)
|
||||
async domains(serverId) { const s = serverRow(serverId); return (await call(fetchImpl, setupOf(s) + '/api/setup/domains', { headers: setupHeaders(s) })).json.domains; },
|
||||
async domains(serverId) { const s = serverRow(serverId); return (await setupCall(s, '/api/setup/domains', { headers: setupHeaders(s) })).json.domains; },
|
||||
async addDomain(serverId, { domain, hosts = null, mailbox = 'admin', adminPassword, ariweb = null, operating = 'active', note = '' }) {
|
||||
const s = serverRow(serverId); const pw = adminPassword || randomBytes(12).toString('base64url');
|
||||
const r = await call(fetchImpl, setupOf(s) + '/api/setup/domains', { method: 'POST', headers: setupHeaders(s), body: { domain, hosts: hosts || [`mail.${domain}`], mailbox, adminPassword: pw, ariweb, operating, note, copyFromPrimary: true } });
|
||||
const r = await setupCall(s, '/api/setup/domains', { method: 'POST', headers: setupHeaders(s), body: { domain, hosts: hosts || [`mail.${domain}`], mailbox, adminPassword: pw, ariweb, operating, note, copyFromPrimary: true } });
|
||||
hostCache.delete(s.id + '|' + domain);
|
||||
if (r.json.initialAdmin) api.storeAdmin(s.id, domain, r.json.initialAdmin.login, pw);
|
||||
return { entry: r.json.domain, initialAdmin: r.json.initialAdmin ? { login: r.json.initialAdmin.login } : null };
|
||||
},
|
||||
async updateDomain(serverId, domain, patch) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await call(fetchImpl, setupOf(s) + '/api/setup/domains/' + encodeURIComponent(domain), { method: 'PATCH', headers: setupHeaders(s), body: patch })).json.domain; },
|
||||
async removeDomain(serverId, domain) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await call(fetchImpl, setupOf(s) + '/api/setup/domains/' + encodeURIComponent(domain), { method: 'DELETE', headers: setupHeaders(s) })).json.removed; },
|
||||
async updateDomain(serverId, domain, patch) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await setupCall(s, '/api/setup/domains/' + encodeURIComponent(domain), { method: 'PATCH', headers: setupHeaders(s), body: patch })).json.domain; },
|
||||
async removeDomain(serverId, domain) { const s = serverRow(serverId); hostCache.delete(s.id + '|' + domain); return (await setupCall(s, '/api/setup/domains/' + encodeURIComponent(domain), { method: 'DELETE', headers: setupHeaders(s) })).json.removed; },
|
||||
/** 도메인 설정(비밀 가림)·메타(DNS 레코드·발신 상태·호스트). */
|
||||
async domainConfig(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/config?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json; },
|
||||
async setDomainConfig(serverId, domain, values) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/config?domain=${encodeURIComponent(domain)}`, { method: 'PUT', headers: setupHeaders(s), body: { values } })).json; },
|
||||
async domainConfig(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/config?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json; },
|
||||
async setDomainConfig(serverId, domain, values) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/config?domain=${encodeURIComponent(domain)}`, { method: 'PUT', headers: setupHeaders(s), body: { values } })).json; },
|
||||
/** 도메인별 보장량(§13.3): 메일함 수·저장 공간·월간 통수를 AriMail 설정으로 */
|
||||
setDomainLimits(serverId, domain, { mailboxes, storageBytes, monthlyMessages }) { return api.setDomainConfig(serverId, domain, { LIMIT_ACCOUNTS: mailboxes ?? '', LIMIT_TOTAL_BYTES: storageBytes != null ? String(storageBytes) : '', LIMIT_MONTHLY_MESSAGES: monthlyMessages ?? '' }); },
|
||||
async plan(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/plan?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json.plan; },
|
||||
async runPlan(serverId, domain, body = {}) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/plan/run?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body })).json; },
|
||||
async dnsProbe(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/dns/probe?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: {} })).json.probe; },
|
||||
async outboundCheck(serverId, domain) { const s = serverRow(serverId); return (await call(fetchImpl, `${setupOf(s)}/api/setup/check?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: { what: 'outbound' } })).json.outboundStatus; },
|
||||
async plan(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/plan?domain=${encodeURIComponent(domain)}`, { headers: setupHeaders(s) })).json.plan; },
|
||||
async runPlan(serverId, domain, body = {}) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/plan/run?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body })).json; },
|
||||
async dnsProbe(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/dns/probe?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: {} })).json.probe; },
|
||||
async outboundCheck(serverId, domain) { const s = serverRow(serverId); return (await setupCall(s, `/api/setup/check?domain=${encodeURIComponent(domain)}`, { method: 'POST', headers: setupHeaders(s), body: { what: 'outbound' } })).json.outboundStatus; },
|
||||
// ---- 관리자 API(메일함)
|
||||
async listUsers(serverId, domain) { const s = serverRow(serverId); return (await admin(s, domain, 'admin/users')).json; },
|
||||
async createUser(serverId, domain, { login, name = '', password, quotaBytes = undefined, services = undefined, role = 'user' }) { const s = serverRow(serverId); return (await admin(s, domain, 'admin/users', { method: 'POST', body: { login, name, password, role, ...(quotaBytes !== undefined ? { quotaBytes } : {}), ...(services ? { services } : {}) } })).json; },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* portal/mail/model.mjs — 메일 영역 세부 표(§15~§18, §23.2). 중심 표(mail_services·mailboxes·domains)는 portal/core/model.mjs 에 있다.
|
||||
*
|
||||
* mail_servers 운영자가 등록한 AriMail 서버(공개 주소·내부 주소·전체 설정 주소·메일 프로그램 접속 호스트). 비밀(설정 토큰·사용량 키·SSO 비밀)은 보관소 'arimail:<id>:*'.
|
||||
* mail_servers 운영자가 등록한 AriMail 서버(공개 주소·내부 주소·전체 설정 주소·설정 API Host(setup_host: AriMail 전체 설정 API 는 기본 도메인 호스트에서만 응답한다 — 실제 환경 결함 수정)·메일 프로그램 접속 호스트). 비밀(설정 토큰·사용량 키·SSO 비밀)은 보관소 'arimail:<id>:*'.
|
||||
* mail_tests 실제 외부 송신·수신·회신 확인 기록(활성화·전환 전후·수동). 단계와 결과만, 메일 내용은 없음.
|
||||
* mail_invites 메일함 초대(초기 비밀번호 대신 링크로 비밀번호를 정함): 토큰 해시·만료·사용 완료·재발송 횟수.
|
||||
* mail_aliases 별칭·전달 전용 주소 기록(정책 미확정이면 생성 불가, §16.2).
|
||||
@@ -36,4 +36,4 @@ CREATE TABLE IF NOT EXISTS mail_usage_log (
|
||||
id TEXT PRIMARY KEY, mail_service_id TEXT NOT NULL, group_id TEXT NOT NULL, subject_type TEXT NOT NULL, subject_id TEXT NOT NULL, storage_bytes INTEGER, monthly_messages INTEGER, mailboxes INTEGER, warn_level INTEGER, limited_json TEXT, measured_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mail_usage_log_subject ON mail_usage_log(subject_type, subject_id, measured_at);
|
||||
` }];
|
||||
` }, { id: 'mail_0002_setup_host', sql: `ALTER TABLE mail_servers ADD COLUMN setup_host TEXT;` }];
|
||||
|
||||
Reference in New Issue
Block a user