02. 모듈 사이트 구축 명세
작성 2026-10-04 · 따르는 표준: 01_모듈_표준.md (표준 1) 작업 장소: 라이센스 서버 저장소 hnbbos/isolo_lic (lic.isolo.co.kr)
쉬운 요약
- 모듈 사이트는 「어떤 모듈이 있고, 몇 번까지 나왔고, 어느 프로그램이 몇 번을 쓰고, 검사는 통과했는가」를 관리하는 곳입니다.
- 새 사이트를 따로 만들지 않고, lic.isolo.co.kr 관리자 화면에 「모듈」 메뉴를 추가합니다. 제품·고객·서명 키·업데이트 기능이 이미 거기 있기 때문입니다.
- 라이센스 보호 장치·설치기·업데이트 도우미는 이 사이트에 올라가는 「기본 모듈」입니다(01 §2.5). 단독 설치로 파는 제품은 이 셋이 빠지면 묶음을 만들 수 없습니다.
- 사이트는 앱 스토어의 진열대 역할을 합니다. 이름표·번호·검사 결과·서명·안내서를 다루고, 실제 코드 파일은 GitHub 창고(
hnbbos/isolo_modules, GitHub Packages, ghcr.io)에 둡니다. 사이트에서 고르면modctl이 창고에서 받아 옵니다. - 발행하면 라이센스 서버의 서명 키로 서명합니다. 고객 서버로 가는 묶음도 같은 키로 서명합니다.
- 함께 쓰는 명령줄 도구
modctl로 만들기·검사·가져오기·새 버전 받기·보고를 합니다.
0. Claude 요청문 (isolo_lic 저장소에서 붙여 넣기)
lic.isolo.co.kr(isolo_lic) 에 「모듈」 메뉴를 추가해 줘.
명세: docs/module-platform/02_모듈사이트_구축.md, 지킬 약속: 01_모듈_표준.md (이 두 파일을 먼저 끝까지 읽어).
[순서]
1. 먼저 지금 구조를 읽고 보고해: index.js 의 표 만드는 방식(CREATE TABLE IF NOT EXISTS)과 PG 경로(db.js shim),
requireAdmin / requireRoleAdmin / resolveAuth(api_keys scope), auditLicense, lib/vendor/vendor-sign.js(signPayload·canonical),
public/admin.html 의 탭 방식(data-tab / switchTab / #tab-*) 과 public/admin/js/pages.js.
2. 명세 §13 의 「1단계」만 만든다. 2·3단계는 하지 마.
화면은 docs/module-platform/design/모듈센터_시안.html 과 design/screens/*.png 를 그대로 따른다(§4.0 디자인 표).
3. §12 의 검사 항목 중 1단계 해당 것을 scripts/ 아래 검사 스크립트로 만들고 MySQL·PG 두 경로에서 돌려 결과(통과/실패 수)를 보고해.
[지킬 것]
- 기존 기능(라이센스 발급·검증·업데이트·.lic)은 동작이 바뀌면 안 된다. 기존 e2e 검사(scripts/e2e-test.js)도 그대로 통과해야 한다.
- 서명 개인키는 지금처럼 다룬다(VENDOR-KEY-RUNBOOK.md). 키가 없으면 발행을 막고(503), 서명 없는 발행은 절대 만들지 않는다.
- 안내서(INTEGRATION.md)를 화면에 보여 줄 때는 반드시 정화(XSS)한다.
- ON DUPLICATE KEY UPDATE 는 쓰지 않는다(PG shim 미지원) — vendor_instances 처럼 SELECT 후 UPDATE/INSERT.
- 운영 서버 배포는 하지 말고, 바뀐 파일·새 표·새 API·검사 결과를 보고해.
1. 목표와 하지 않는 것
목표
- 모듈 목록과 버전(검사 중·발행·회수)을 한곳에서 봅니다.
- 어느 프로그램이 어느 모듈의 몇 번을 쓰는지, 누가 뒤처졌는지 봅니다.
- 새 버전을 모든 사용처의 검사를 통과해야 발행하고, 발행은 서명합니다.
- 사용처에 「새 번호 나옴 / 이 번호 회수됨」을 알립니다.
- 고객 서버로 보낼 묶음을 만들어 서명하고, 기존 업데이트 기능에 연결합니다.
- 나혼자워크 패키지 센터에 고객용 목록(카탈로그)을 줍니다.
- 기본 모듈(라이센스 보호 장치·설치기·업데이트 도우미)을 지정하고, 단독 설치 제품이 빠뜨리지 못하게 막습니다.
하지 않는 것
- 소스 코드 보관 (GitHub가 원본)
- 서버에서 올라온 코드 실행 (검사는 GitHub Actions나 각 프로그램에서)
- 고객 서버에 원격으로 명령 내리기 (고객 쪽 업데이트 도우미가 가져가는 방식만)
- 결제 (기존 주문·결제 기능을 그대로 씀)
2. 왜 lic.isolo.co.kr 안에 두나 — 그대로 쓰는 것
| 이미 있는 것 (isolo_lic) | 모듈 사이트에서 쓰는 곳 |
|---|---|
products 표 (product_code) | 사용처 = 제품. 사용처 이름은 제품 코드로 통일 |
api_keys 연동 키 + resolveAuth (scope read/full) | CI·도구 인증. scope modules를 새로 추가(§9) |
requireAdmin / requireRoleAdmin (x-admin-key) | 화면·관리 API. 발행·회수·묶음은 requireRoleAdmin |
auditLicense() → license_audit_log | 모든 쓰기를 action: 'module.*'로 기록 |
lib/vendor/vendor-sign.js signPayload()·canonical + 키링 | 발행 기록·묶음 서명 |
lib/vendor/license-guard.js publicKeysB64() | 공개키 목록 제공 |
update_packages · /api/update/check · /api/update/report | 묶음을 업데이트 패키지로 연결 |
알림 설정(admin_settings의 notify_*, notification_logs) | 발행·회수 알림 |
public/admin.html 탭 방식 (<a data-tab="…" onclick="switchTab('…')"> + <div class="tab-page" id="tab-…">), public/admin/js/pages.js | 「모듈」 탭 추가 |
db.js MySQL/PG 겸용 shim | 새 표도 두 경로에서 돌아야 함 |
3. 전체 흐름
[개발자] isolo_modules 에서 고침 → modctl lint·test → 번호 올림 → 태그 license-guard-v1.5.0 push
│
[GitHub Actions (isolo_modules)]
│ 단위·약속 검사 → npm 패키지 게시 / 이미지 게시(ghcr) → 지문 수집
│ modctl register ─────────────────────────► POST /api/modules/register (상태: 검사 중)
│ (3단계) 사용처 저장소들에 「검사해 줘」 신호(repository_dispatch)
│
[각 프로그램 저장소 CI] 새 번호로 바꿔 자기 검사 → modctl check-report ─► POST …/checks
│
[관리자 — 모듈 탭] 호환표 확인 → 「발행」 ─► 서명 · 상태: 발행 · 알림
│
[각 프로그램] modctl outdated / update → 검사 → 커밋 → modctl report ─► POST /api/modules/report
│
[관리자 — 묶음] 제품 버전 + 모듈 번호 + 이미지 지문 → 서명 → update_packages 연결
│
[고객 서버 업데이트 도우미] 묶음 받기 → 서명·typ 확인 → 지문으로 이미지 받기 → 백업 → 교체 → 보고
4. 화면 — 「모듈 센터」
4.0 디자인과 위치
- 디자인 시안:
design/모듈센터_시안.html(브라우저로 열면 화면을 오갈 수 있음) · 화면 캡처design/screens/10장(PC 8장 + 모바일 2장) 01_홈·02_모듈상세_개요·03_모듈상세_버전·04_모듈상세_사용처·05_모듈상세_호환표·06_모듈상세_안내서·07_발행창·08_사용처_묶음·09_모바일_홈·10_모바일_상세- 같은 집안 디자인: 매뉴얼 센터(man.isolo.co.kr)·버그개선(bug.isolo.co.kr)과 맞춥니다. 2026-10-04 두 사이트의 화면과 계산된 스타일을 확인해 가져왔습니다.
| 요소 | 값 | 출처 |
|---|---|---|
| 글꼴 | Pretendard | 두 사이트 공통 |
| 강조색 | #3B6FE0 (연한 바탕 #eaf0fc) | 매뉴얼 센터 --accent |
| 글자 / 흐린 글자 / 테두리 / 옆판 | #2f3437 / #908e89 / #ededec / #fbfbfa | 매뉴얼 센터 변수 |
| 영웅 카드 | 둥글기 22px, 그림자 0 14px 44px rgba(0,0,0,.07) | 매뉴얼 센터 첫 화면 |
| 모듈 카드 | 둥글기 16px, 위 4px 색띠, 올리면 떠오름 | 매뉴얼 센터 home-card |
| 모듈 배지 | 남색 그라데이션 #22325b → #111c33, 48px·둥글기 14px | 매뉴얼 센터 「EP」 배지 |
| 상태 배지 | 빨강 #fce8e6/#c5221f, 노랑 #fef7e0/#b06000, 초록·파랑 | 버그개선 badge |
| 통계 숫자 | 36px 그라데이션 글자 | 버그개선 stat-num |
| 활동판 | 머리줄 + LIVE 점 + 줄 목록 | 버그개선 커뮤니티 보드 |
| 아이콘 | Material Symbols Outlined | 버그개선 |
| 상세 화면 | 왼쪽 옆판(현재 항목 강조) + 경로 + 제목 + 버전 알약 | 매뉴얼 센터 문서 화면 |
- 종류별 색띠: 부품
#3B6FE0, 서비스#12a37f, 시작 틀#7b5ce0, 기본 모듈은 남색 - 위치: lic 서버가
https://lic.isolo.co.kr/modules/로 제공합니다(정적 HTML+JS, 데이터는 §6 API). 기존 관리자 로그인(세션)을 그대로 쓰기 위해 같은 주소 아래에 둡니다. 기존public/admin.html에는 탭data-tab="modules"를 추가하되 「모듈 센터 열기」 링크만 둡니다. 따로 주소가 필요하면mod.isolo.co.kr를 같은 서버로 연결합니다. - 시안의 데이터는 예시입니다. 실제 화면은 §6 API에서 읽어 옵니다.
4.1 모듈 목록
| 칸 | 내용 |
|---|---|
| 이름 · id | 라이센스 보호 장치 · license-guard |
| 종류 | 부품 / 서비스(중앙·곁붙이) / 시작 틀 |
| 기본 | 「기본 · 단독 필수」 표시 (required_for) |
| 최신 발행 | 1.5.0 (발행일) |
| 검사 중 | 1.6.0-rc.1 등 |
| 사용처 | 4곳 |
| 뒤처짐 | 2곳 (주황) · 회수된 번호를 쓰는 곳 1곳 (빨강) |
거르기: 종류 · 「뒤처진 곳 있음」 · 「회수 번호 사용 중」
4.2 모듈 상세 — 탭 6개
- 개요: 이름표 요약(연결부·설정 칸·바깥 접속·회사 격리), 원본 위치 링크
- 버전: 번호·상태·변경 크기(patch/minor/major)·발행자·발행일·「사용처가 할 일」
- 사용처: 제품별 사용 번호, 마지막 보고, 임시 덧댐 여부, 뒤처짐
- 호환표: 행 = 버전, 열 = 사용처, 칸 = 검사 결과(통과 수/실패 수, 기록 링크)
- 안내서:
INTEGRATION.md표시(정화 필수) + 「Claude 요청문 복사」 버튼 - 기록: 이 모듈의 감사 기록
4.3 발행 화면
- 검사 결과 요약: 모든 사용처 통과 → 「발행」 버튼 활성화
- 검사 없음·실패가 있으면 버튼이 비활성입니다. 관리자 역할만 「사유 쓰고 강제 발행」을 할 수 있습니다(사유 10자 이상, 기록에 남음).
- MAJOR이면 경고 문구와 함께 「사용처가 할 일」을 다시 보여 줍니다.
4.4 회수
- 사유 필수. 이 번호를 쓰는 사용처와, 이 번호가 들어간 묶음을 함께 보여 줍니다.
4.5 묶음
- 제품 선택 → 묶음 번호 → 이미지 지문 입력(또는 CI가 등록) → 모듈 번호(그 제품의 마지막 보고값을 기본으로) → 확인 → 서명
- 「업데이트 패키지로 등록」 체크 시
update_packages에 한 줄을 만듭니다.
4.6 제품 화면과 연결
- 기존 제품 상세에 설치 형태(SaaS / 단독 / 둘 다) 선택을 추가합니다(
products.deploy_modes). - 기존 제품 상세에 「쓰는 모듈」 표를 추가합니다(모듈 · 번호 · 뒤처짐).
- 단독 설치 제품인데 기본 모듈이 빠져 있으면 빨간 줄로 「기본 모듈 없음: installer」처럼 보여 줍니다.
5. DB 표
기존 표(products 등)와 같은 방식으로 시작할 때 CREATE TABLE IF NOT EXISTS로 만듭니다. PG 경로는 기존 표를 PG에서 어떻게 만드는지 먼저 확인하고 똑같이 따릅니다. ENUM 대신 VARCHAR(값 제약은 앱에서), ON DUPLICATE KEY UPDATE 금지, 묶음 고유 제약은 양쪽 공통인 UNIQUE (a, b) 문법을 씁니다.
-- 모듈 (이름표의 바뀌지 않는 부분 + 화면용 캐시)
CREATE TABLE IF NOT EXISTS modules (
id INT AUTO_INCREMENT PRIMARY KEY,
module_id VARCHAR(40) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
kind VARCHAR(10) NOT NULL, -- part | service | starter
summary VARCHAR(500) NOT NULL DEFAULT '',
owner VARCHAR(100) NOT NULL DEFAULT '',
source_repo VARCHAR(200) NOT NULL,
source_path VARCHAR(300) NOT NULL DEFAULT '',
latest_version VARCHAR(40) DEFAULT NULL, -- 발행된 것 중 가장 높은 번호(캐시)
required_for VARCHAR(10) NOT NULL DEFAULT '', -- 기본 모듈: '' 선택 | 'onprem' 단독 설치 제품 필수 | 'all' 모든 제품 필수
catalog JSON DEFAULT NULL, -- module.json 의 catalog
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 버전 (발행 후에는 바꾸지 않음 — 회수 표시만)
CREATE TABLE IF NOT EXISTS module_versions (
id INT AUTO_INCREMENT PRIMARY KEY,
module_id VARCHAR(40) NOT NULL,
version VARCHAR(40) NOT NULL,
status VARCHAR(12) NOT NULL DEFAULT 'testing', -- testing | released | withdrawn
bump VARCHAR(6) NOT NULL DEFAULT '', -- patch | minor | major (서버가 계산)
suspect_bump BOOLEAN DEFAULT FALSE, -- 연결부·설정·표가 바뀌었는데 patch 로 올린 경우
manifest JSON NOT NULL,
manifest_sha256 CHAR(64) NOT NULL,
artifacts JSON NOT NULL, -- npm / git / image (01 §14.1)
changelog TEXT, -- 이 번호의 변경 기록(60KB 이하)
integration_md TEXT, -- 안내서 원문(60KB 이하, 보여 줄 때 정화)
payload JSON DEFAULT NULL, -- 서명한 내용 그대로
signature TEXT DEFAULT NULL,
note VARCHAR(500) DEFAULT '', -- 강제 발행·회수 사유
created_by VARCHAR(100) DEFAULT '',
released_by VARCHAR(100) DEFAULT '',
released_at TIMESTAMP NULL DEFAULT NULL,
withdrawn_at TIMESTAMP NULL DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (module_id, version)
);
-- 사용처 검사 결과 (이력으로 쌓음 — 같은 버전·사용처는 마지막 것이 유효)
CREATE TABLE IF NOT EXISTS module_checks (
id INT AUTO_INCREMENT PRIMARY KEY,
module_id VARCHAR(40) NOT NULL,
version VARCHAR(40) NOT NULL,
product_code VARCHAR(50) NOT NULL,
product_version VARCHAR(40) DEFAULT '',
runtime VARCHAR(10) DEFAULT '',
passed INT DEFAULT 0,
failed INT DEFAULT 0,
ok BOOLEAN NOT NULL,
log_url VARCHAR(500) DEFAULT '',
reported_by VARCHAR(100) DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 사용처 (각 프로그램의 modules.lock.json 보고값)
CREATE TABLE IF NOT EXISTS module_consumers (
id INT AUTO_INCREMENT PRIMARY KEY,
module_id VARCHAR(40) NOT NULL,
product_code VARCHAR(50) NOT NULL,
version_in_use VARCHAR(40) NOT NULL,
runtime VARCHAR(10) DEFAULT '',
patched BOOLEAN DEFAULT FALSE, -- 임시 덧댐 있음
patch_remove_by DATE DEFAULT NULL,
repo VARCHAR(200) DEFAULT '',
reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (module_id, product_code)
);
-- 묶음 (고객 서버로 보내는 세트)
CREATE TABLE IF NOT EXISTS module_bundles (
id INT AUTO_INCREMENT PRIMARY KEY,
product_code VARCHAR(50) NOT NULL,
bundle_version VARCHAR(40) NOT NULL,
payload JSON NOT NULL, -- 01 §14.2 그대로
signature TEXT NOT NULL,
update_package_id INT DEFAULT NULL,
status VARCHAR(12) DEFAULT 'active', -- active | withdrawn
created_by VARCHAR(100) DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (product_code, bundle_version)
);
기존 products 표에는 칸 하나를 더합니다. 기존 productNewCols 방식(시작 시 ALTER TABLE … ADD COLUMN, 이미 있으면 무시)을 따릅니다.
-- 설치 형태: 'saas' | 'onprem' | 'saas,onprem' (단독이 들어 있으면 기본 모듈 필수)
ALTER TABLE products ADD COLUMN deploy_modes VARCHAR(20) DEFAULT 'saas';
감사 기록은 새 표를 만들지 않고 auditLicense({ action: 'module.<동작>', detail })로 남깁니다(licenseId는 비움).
6. API
6.1 관리자 화면용 — requireAdmin (★ = requireRoleAdmin)
| 방법 | 주소 | 하는 일 |
|---|---|---|
| GET | /api/modules | 목록 + 최신 발행·검사 중·사용처 수·뒤처진 수 |
| GET | /api/modules/:id | 상세: 버전들, 사용처, 호환표 |
| GET | /api/modules/:id/versions/:ver | 버전 상세(이름표·변경 기록·검사·서명) |
| PATCH | /api/modules/:id | 카탈로그·담당·활성 수정 (이름표의 다른 칸은 못 바꿈). required_for(기본 모듈 지정) 변경은 ★ |
| POST ★ | /api/modules/:id/versions/:ver/release | 발행. 본문 { force?: boolean, note?: string } |
| POST ★ | /api/modules/:id/versions/:ver/withdraw | 회수. 본문 { note } 필수 |
| GET | /api/modules/consumers?product= | 제품이 쓰는 모듈 |
| GET | /api/modules/bundles?product= | 묶음 목록 |
| POST ★ | /api/modules/bundles | 묶음 만들기·서명 (§8) |
6.2 CI·도구용 — requireModulesKey (연동 키 scope modules 또는 full)
| 방법 | 주소 | 하는 일 |
|---|---|---|
| POST | /api/modules/register | 이름표 + 산출물 + 안내서 + 변경 기록 → 모듈이 없으면 만들고, 버전을 「검사 중」으로 등록 |
| POST | /api/modules/:id/versions/:ver/checks | 사용처 검사 결과(01 §11.4 형식) |
| POST | /api/modules/report | 프로그램의 modules.lock.json 보고 → module_consumers 갱신 |
| GET | /api/modules/:id/released | 발행된 번호 목록(회수 표시 포함) |
| GET | /api/modules/updates?product= | 이 제품이 받을 수 있는 새 번호 — 모듈마다 patch / minor / major / withdrawnInUse |
6.3 공개 — 인증 없음, 속도 제한
| 방법 | 주소 | 하는 일 |
|---|---|---|
| GET | /api/public/modules/keys | 서명 확인용 공개키 목록(publicKeysB64()). 받는 쪽은 이 목록을 믿지 않고 내장 키와 대조만 합니다 |
| GET | /api/public/modules/catalog | 고객 패키지 센터용 목록 (§10) |
| GET | /api/public/modules/bundles/:product/:ver | 묶음 서명본. X-License-Key가 그 제품의 유효한 라이센스일 때만 줌 |
6.4 등록 요청 예 (POST /api/modules/register)
{
"manifest": { "moduleStandard": 1, "id": "license-guard", "version": "1.5.0", "kind": "part", "...": "..." },
"artifacts": {
"npm": { "name": "@hnbbos/license-guard", "version": "1.5.0", "integrity": "sha512-…" },
"git": { "repo": "hnbbos/isolo_modules", "tag": "license-guard-v1.5.0", "commit": "0a1b…(40자리)" }
},
"changelog": "## 1.5.0 — 2026-10-20\n### 새 기능\n- …\n### 사용처가 할 일\n- 없음",
"integration": "# 라이센스 보호 장치 붙이기 안내서 …"
}
응답: 201 { "module": "license-guard", "version": "1.5.0", "status": "testing", "bump": "minor", "suspectBump": false, "consumers": ["nahonza-work", "zerolabel-saas"] }
7. 등록·발행 규칙
7.1 등록할 때 확인 (어기면 거절)
- 이름표가 표준 형식에 맞는지(
schemas/module-1.json):moduleStandard,id형식,kind, 버전 형식, 필수 칸. 틀리면400 MANIFEST_INVALID+ 틀린 칸 목록. - 같은 번호는 상태와 상관없이 다시 등록할 수 없습니다 →
409 VERSION_EXISTS. - 같은 MAJOR 줄에서 가장 높은 번호보다 커야 합니다 →
409 VERSION_NOT_GREATER. 옛 줄의 보안 고침은 허용합니다(01 §7). - 종류별 산출물 필수
- 부품:
npm또는git(태그 + 40자리 커밋) - 서비스:
image(@sha256:지문 필수) - 시작 틀:
git - 없으면
400 ARTIFACT_MISSING. - 크기 제한: 이름표 64KB, 안내서 60KB, 변경 기록 60KB.
- 변경 크기 계산: 직전 발행 번호와 비교해
bump를 정합니다. 연결부·설정·provides.tables가 바뀌었는데 patch면suspect_bump = true로 표시하고 화면에 경고합니다. - 이미 있는 모듈의
kind·source가 바뀌면 거절합니다(409 MODULE_IDENTITY_CHANGED).
7.2 발행할 때 확인
- 상태가 「검사 중」이어야 합니다.
- 사용처 =
module_consumers에 있는 제품 + 이름표compat에 적힌 제품. - 사용처마다 이 번호의 마지막 검사 결과가 통과여야 합니다. 아니면
409 CHECKS_MISSING/409 CHECKS_FAILED(어느 사용처인지 포함). force: true는 관리자 역할 +note10자 이상일 때만 허용하고, 기록에 남깁니다.- 사용처가 아직 하나도 없으면(첫 발행) 모듈 자체 검사 결과만으로 발행할 수 있습니다.
- 서명 키가 없으면
503 SIGNING_KEY_MISSING입니다. 서명 없는 발행은 만들지 않습니다. - 서명 내용은 01 §14.1의
typ: "isolo.module-release/1"형식이고,vendor-sign.js의signPayload()로 서명합니다. - 성공하면
status=released,modules.latest_version을 갱신하고, 알림을 보내고(§9.3), 기록을 남깁니다.
7.3 회수
- 「발행」 상태만 회수할 수 있습니다. 사유는 필수입니다.
- 회수된 번호는
updates에 나오지 않습니다. 그 번호를 쓰는 사용처에는withdrawnInUse로 표시되고 알림이 갑니다. - 그 번호가 들어간 묶음은 화면에서 빨간색으로 표시됩니다. 묶음 회수는 따로 합니다.
8. 묶음 (고객 서버용)
요청: POST /api/modules/bundles (관리자 역할)
{
"product": "nahonza-work",
"bundleVersion": "1.8.0",
"minFrom": "1.6.0",
"images": {
"server": "ghcr.io/hnbbos/nahonza-server@sha256:…",
"client": "ghcr.io/hnbbos/nahonza-client@sha256:…"
},
"modules": { "license-guard": "1.5.0" },
"registerUpdate": true
}
규칙
- 이미지 값은 모두
@sha256:을 포함해야 합니다(이름표:latest거절). modules의 번호는 모두 발행 상태여야 합니다(회수·검사 중 거절).modules를 생략하면 그 제품의 마지막 보고값을 씁니다.- 기본 모듈 확인: 묶음은 단독 설치용이므로,
required_for가onprem또는all인 모듈이 모두modules에 발행 번호로 들어 있어야 합니다. 빠지면409 BASE_MODULE_MISSING(빠진 모듈 목록 포함)입니다. 제품의deploy_modes에onprem이 없으면 묶음을 만들 수 없습니다(409 PRODUCT_NOT_ONPREM). - 01 §14.2 형식(
typ: "isolo.bundle/1")으로 서명하고module_bundles에 저장합니다. registerUpdate: true이면update_packages에 한 줄을 만듭니다.product_code= 제품,version= 묶음 번호,download_url= 공개 묶음 주소,file_sha256= 서명 내용의 sha256- 기존
/api/update/check가 그대로 「새 버전 있음」을 알려 줍니다. - 진품 확인은 기존 HMAC 서명(고객 쪽에서 확인 불가)이 아니라 묶음의 Ed25519 서명으로 합니다.
9. 인증·보안
9.1 연동 키 scope modules
- 기존
api_keys.scope는read/full입니다.modules를 추가하고, 연동 키 만들기 화면의 선택지에도 넣습니다. requireModulesKey는scope ∈ {modules, full}일 때 §6.2 주소만 통과시킵니다.modules키로 §6.1(관리자) 주소에 오면403입니다.- CI에는 GitHub Actions 비밀값
LIC_MODULES_KEY로 넣습니다. 도구는 이 키를 화면이나 기록에 출력하지 않습니다. - 참고: 지금
api_keys는 키 원문을 저장합니다(WHERE api_key = ?). 이번 작업 범위는 아니지만 해시 저장으로 바꾸는 개선을 따로 권합니다.
9.2 그 밖의 규칙
- 안내서·변경 기록을 화면에 보여 줄 때 정화합니다.
<script>,on*=속성,javascript:/data:링크,<iframe>을 제거합니다. - 사이트는 올라온 코드를 실행하지 않습니다. 이름표는 JSON 형식 검사만 합니다.
- 쓰기 API는 모두
auditLicense로 기록합니다. 기록에는 누가(키 이름/관리자), 무엇을(모듈·번호), 왜(사유)가 들어갑니다. - 공개 API에는 기존 속도 제한 방식(예:
limitUpdate)과 같은 제한을 겁니다. - 서명 개인키 취급은 기존
VENDOR-KEY-RUNBOOK.md를 따릅니다(키 백업·교체·키링).
9.3 알림
- 발행: 「
license-guard 1.5.0발행 — 뒤처진 곳: 제로라벨(1.4.0)」 - 회수: 「
license-guard 1.5.0회수(사유) — 쓰는 곳: 나혼자워크」 - 임시 덧댐 기한 지남: 「나혼자워크의 license-guard 임시 덧댐 기한(11/01) 지남」
- 기존 알림 채널 설정을 그대로 쓰고,
notification_logs에 남깁니다.
10. 카탈로그 — 나혼자워크 패키지 센터와 연결
나혼자워크는 LICENSE_CATALOG_URL에서 { programs: [...] }를 받아 normalizeCatalog()로 정리합니다 (server/src/controllers/integration.controller.ts). 이 모양에 맞춰 줍니다.
GET /api/public/modules/catalog (요청 머리 X-License-Key 선택)
{
"programs": [
{
"appKey": "messenger",
"name": "팀 메신저",
"description": "대화방·공지·첨부·실시간 알림",
"icon": "message-circle",
"category": "업무",
"baseUrl": "https://messenger.isolo.co.kr/embed/messenger",
"tier": "licensed",
"licenseRequired": true,
"licensed": false,
"link": "https://lic.isolo.co.kr/modules/messenger"
}
]
}
kind = service이고catalog.visible = true인 모듈만 보여 줍니다. 부품·시작 틀은 고객에게 보이지 않습니다.licensed:X-License-Key가 유효하고 그 라이센스의 기능 목록에catalog.featureKey가 있으면true, 아니면false.baseUrl은 나혼자워크와 다른 원본(주소)이어야 합니다(01 §10-9).
10-2. 패키지 카탈로그 (1.26.0) — 기능 패키지 + 외부 프로그램, 서명
나혼자워크 1.9 부터 메뉴를 「기본 프로그램」(메일·캘린더·주소록·할일)과 「추가 제품」으로 나누고, 추가 제품 목록을 이 사이트에서 받습니다.
GET /api/public/packages/catalog?product=<제품 코드>&appVersion=<판> (요청 머리 X-License-Key 선택)
{
"catalog": {
"typ": "isolo.package-catalog/1", "product": "nahonza-work", "generatedAt": "…",
"packages": [
{ "key": "estimates", "name": "견적서", "summary": "…", "features": ["…"], "tier": "free", "licenseRequired": false, "licensed": true, "version": "1.9.1" },
{ "key": "places", "name": "내 장소", "tier": "free", "licenseRequired": false, "licensed": true, "hidden": true },
{ "type": "external", "appKey": "erp", "name": "회계 ERP", "baseUrl": "https://erp.example.com/embed", "tier": "licensed", "licenseRequired": true, "licensed": false }
]
},
"signature": "<base64 Ed25519(canonical(catalog))>", "keyId": "…"
}
- 관리: 화면 「패키지」(표
product_packages). 기능 패키지(kind builtin)는 제품 코드 안의 메뉴라 지우지 않고 숨김·내림만, 주소 없음. 외부 프로그램(kind external)은 https 주소 필수. licensed: 무료는 늘 true. 「라이센스」 조건은X-License-Key가 「라이센스 허용」(표package_grants, 키 sha256)에 있을 때만 true.- 웹훅:
package.released·package.updated·package.withdrawn·license.changed(02 §9 와 같은 서명 머리). 받는 쪽은 서명 확인 뒤 카탈로그를 다시 받는다. - 설치 현황:
POST /api/connect/package-installs(연동 키 scope modules) —{ product, appVersion, scope(회사 id 해시), packages[], externals[] }. 표package_installs. - 받는 쪽 약속(나혼자워크):
docs/운영/패키지센터_연동.md— 공개키는 받는 쪽 설정(PACKAGE_CENTER_PUBKEYS)과만 대조, 서명이 없으면 「더 막는 쪽」 값만 받음.
기록 (1.27.0)
| 표 | 무엇을 | 언제 쌓이나 |
|---|---|---|
package_history | 패키지 하나의 변경 — action(seed·create·update·status·revert·delete·grant·grant.delete), 누가, 바뀐 칸만 before_json/after_json | 넣기·고침·지움·라이센스 허용 때마다 |
package_install_events | 받는 쪽 보고 중 더해진·빠진 패키지(회사는 해시) | package-installs 보고가 이전과 다를 때만 |
package_fetches | 제품·날짜마다 카탈로그를 받아 간 횟수 · 그중 라이센스 키로 받은 수 · 마지막 앱 판 | 공개 카탈로그를 받을 때(하루 한 줄로 모음) |
GET /api/products/:code/packages/:key(관리 키) —{ package, catalogEntry, history[], installs{now,reported,events[],byDay}, deliveries[], grants[] }GET /api/products/:code/package-activity(관리 키) — 제품 전체 최근 기록 40 · 받아 간 횟수 14일 · 설치 바뀜 20POST /api/products/:code/packages/:key/history/:id/revert(관리자) — 그 기록의before값으로 고침.update·status·revert기록만(넣기·지움은 400NOT_REVERTABLE). 되돌림도revert기록으로 남고, 받는 쪽엔 고칠 때와 같은 웹훅이 감- 화면: 「패키지」 카드 →
#package/<제품>/<key>/{overview|history|installs|alerts}. 「활동 기록」의package.*줄을 누르면 그 패키지의 변경 기록으로
연동 설정 · 켜고 끄기 · 쓰는 모듈 (1.59.0)
| 표 | 무엇을 |
|---|---|
package_switches | 라이센스(target_type license, 키 sha256)·설치본(install, 보고한 scope)마다 패키지 켬(1)·끔(0). 줄이 없으면 공개 상태대로 |
package_modules | 패키지가 쓰는 모듈 id — 쓰는 판은 제품의 사용처(module_consumers)에서 읽음 |
package_installs 칸 더함 | fetched_at · fetch_app_version · license_hash · license_hint — 받는 쪽이 ?scope= 를 붙여 받으면 그 설치 줄에 |
package_grants.fetched_at | 그 라이센스 키로 마지막으로 받아 간 때 |
- 카탈로그
?scope=<설치 해시>(선택) — 설치 현황을 보고한 적 있는 scope 만 기록. 답의catalog.scope로 돌아옴(서명 안). 패키지마다enabled— 끔·숨김·내림이면 false(끈 것은hidden:true도 붙여 예전 받는 쪽도 숨김), 「켬」은 숨김 패키지를 그 대상에게만 보이게(내림은 못 되살림). 설치 줄이 라이센스 줄보다 먼저,licensed는 그대로 GET /api/products/:code/package-integration(관리 키) —{ connection(상태 live·stale·none · 받아 간 횟수 · 서명 키 · 웹훅), license(라이센스 서버 대조·제품 코드), packages[](modules[] 쓰는 판→최신·state, moduleState, grants, switchedOff/On, suggested[]), sharedModules[], installs[], licenses[] }. 직원에게는 라이센스 앞 4자만·해시 없음PUT /api/products/:code/package-switches(관리자) —{ target: 'license'|'install', scope | licenseKey | licenseHash, switches: { key: true|false|null } }. null 은 기본으로. 바뀌면package.updated웹훅 · 패키지 기록switchPUT /api/products/:code/packages/:key/modules(관리자) —{ modules: [id…] }통째로(센터에 없는 id 는 400MODULE_UNKNOWN) · 기록modulesPOST /api/products/:code/package-modules/suggest(관리자) — 기본 틀에 적어 둔 추천 모듈 중 센터에 있는 것만 붙임 (「기본 패키지 넣기」 때도 저절로)- 화면: 「패키지」 → 탭 「연동 설정」
#packages/<제품>/link
11. 모듈 도구 modctl
- 위치:
isolo_modules/tools/modctl/(Node 20 이상, 외부 의존 없이fetch·crypto·child_process만 사용) - 배포:
@hnbbos/modctl(GitHub Packages). Python 프로그램 저장소에서도 개발 PC·CI에 Node가 있으면 그대로 씁니다. - 설정:
MODULES_SITE_URL(기본https://lic.isolo.co.kr),LIC_MODULES_KEY(CI 전용),NODE_AUTH_TOKEN·MODULES_READ_TOKEN(비공개 저장소 읽기) - 모든 명령은 실패하면 0이 아닌 값으로 끝나고, 비밀값은 출력하지 않습니다.
11.1 모듈 저장소에서
| 명령 | 하는 일 | ||
|---|---|---|---|
| `modctl init <id> --kind part\ | service\ | starter [--runtime node,python]` | 01 §5 모양의 폴더·이름표·안내서·변경 기록 뼈대 |
modctl lint [<id>] | 이름표 형식 · 번호 일치(package.json/pyproject) · 안내서 필수 목차 · CHANGELOG의 「사용처가 할 일」 · 금지 검사(아래) | ||
modctl test <id> | 단위 검사 + 약속 검사(contract/cases.json을 Node·Python 두 판에 돌려 결과 대조) | ||
| `modctl bump <id> patch\ | minor\ | major` | 번호 올리기(세 파일 동시) + CHANGELOG 머리 추가 + 태그 이름 출력. 태그·push는 사람이 함 |
modctl register <id> | (CI) 산출물 지문을 모아 POST /api/modules/register |
lint의 금지 검사(완벽하지 않은 휴리스틱이며, 걸리면 사람이 확인합니다)
- 모듈 코드가 사용처 경로를 import함 (
../../server/,from app.등 이름표 밖 경로) - 보안 관련 이름(
PUBKEY,PUBLIC_KEY,ENFORCE,PRODUCT_CODE,SECRET)을process.env/os.environ에서 읽음 - iframe에
allow-same-origin과allow-scripts를 함께 줌 - 이름표
network에 없는 고정 주소(https://…)로 접속함
11.2 프로그램 저장소에서
| 명령 | 하는 일 |
|---|---|
modctl add <id>[@<ver>] | 발행된 최신(또는 지정) 번호를 설치합니다. Node는 package.json에 정확한 번호로 넣고 .npmrc 안내, Python은 requirements에 git+…@<id>-v<ver>#subdirectory=… 줄을 넣습니다. modules.lock.json을 기록하고, 호스트 연결 파일 뼈대(연결부 목록대로 빈 함수)를 만듭니다 |
modctl outdated | 뒤처진 모듈 표(patch / minor / major), 회수 번호 사용 경고, 임시 덧댐 기한 경고 |
modctl update <id> [--to <ver>] | 번호 교체 + 그사이 CHANGELOG의 「사용처가 할 일」을 모아 출력. MAJOR는 --yes-major가 있어야 진행 |
modctl verify | lock의 모든 모듈이 사이트에서 발행·서명된 번호인지, 서명이 내장 공개키로 확인되는지 검사 |
modctl check-report <id> --result <파일> | (CI) 사용처 검사 결과 보고 |
modctl report | (CI) modules.lock.json 보고 |
modctl new <starter-id> <폴더> --var KEY=값 … | 시작 틀 복사(자리표시자 치환) + lock에 appliedAt 기록 |
modctl starter-diff | 시작 틀의 새 번호 CHANGELOG 중 아직 반영하지 않은 항목 목록 |
11.3 자동화 (3단계)
- isolo_modules: 태그 push → 검사 → 게시 →
register→ 사용처 저장소마다repository_dispatch(이벤트module-check, 내용{id, version}) - 각 프로그램 저장소
module-check.yml: 그 번호로 바꿔 검사 →check-report(커밋하지 않음) - 각 프로그램 저장소 주간 작업:
modctl outdated→ patch·minor가 있으면 업데이트 PR을 자동으로 엶(MAJOR는 이슈만) - 다른 저장소로 신호를 보내려면 권한이 좁은 토큰(GitHub App 또는 세분화 토큰)이 필요합니다. 대표님이 만들어 비밀값으로 넣습니다.
12. 검사 항목 (만든 뒤 반드시 돌릴 것)
| # | 항목 | 단계 |
|---|---|---|
| 1 | 표준에 안 맞는 이름표 거절: kind 틀림, id 대문자, 번호 형식, moduleStandard 없음 | 1 |
| 2 | 같은 번호 재등록 409 (회수된 번호 포함) | 1 |
| 3 | 같은 MAJOR 줄에서 낮은 번호 409 / 옛 줄 보안 고침은 허용 | 1 |
| 4 | 연동 키 read로 등록 → 403 / modules 키로 발행(관리자 API) → 403 | 1 |
| 5 | 서명 키가 없으면 발행 503, 상태는 그대로 「검사 중」 | 1 |
| 6 | 발행 서명을 공개키로 확인 통과 / 내용 한 글자 바꾸면 실패 / typ 다르면 거절 | 1 |
| 7 | 발행된 버전의 이름표·산출물은 어떤 API로도 바뀌지 않음 | 1 |
| 8 | report로 사용처 갱신(있으면 고침, 없으면 추가) — MySQL·PG 두 경로 모두 | 1 |
| 9 | updates가 patch / minor / major를 맞게 구분 | 1 |
| 10 | 사용처 검사 없음 → 발행 409 CHECKS_MISSING / 실패 → 409 CHECKS_FAILED / 강제는 관리자 역할 + 사유, 기록 남음 | 2 |
| 11 | 회수 후 updates에 안 나오고, 쓰는 곳에 withdrawnInUse | 2 |
| 12 | 연결부가 바뀌었는데 patch → suspectBump = true | 2 |
| 13 | 안내서 표시 XSS: <img src=x onerror=…>, [a](javascript:…), <iframe>이 무력화됨 | 1 |
| 14 | 묶음: 이미지에 지문이 없으면 거절 / 회수·검사 중 모듈 포함 거절 / 서명 확인 통과 | 3 |
| 15 | 공개 묶음: 다른 제품 라이센스 키 → 403, 키 없음 → 401 | 3 |
| 16 | 카탈로그: 서비스·visible만 / 키 없으면 licensed=false / 기능 있으면 true / 나혼자워크 normalizeCatalog가 그대로 받음 | 3 |
| 17 | 모든 쓰기에 감사 기록 | 1 |
| 18 | 공개 API 속도 제한 동작 | 1 |
| 19 | 기존 라이센스·업데이트 검사(scripts/e2e-test.js) 그대로 통과 | 1 |
| 20 | 기본 모듈 지정(required_for)은 관리자 역할만 / 단독 제품 묶음에 기본 모듈이 빠지면 409 BASE_MODULE_MISSING / SaaS 전용 제품 묶음은 409 PRODUCT_NOT_ONPREM | 1(지정)·3(묶음) |
검사는 결과 줄이 없으면 실패로 칩니다. 또 검사 시작 시 지금 도는 서버의 버전을 출력해, 낡은 서버로 통과하는 일을 막습니다.
13. 단계
| 단계 | 만드는 것 | 끝난 기준 |
|---|---|---|
| 1 | 표 5개 + products.deploy_modes · 등록 · 목록/상세(개요·버전·사용처·안내서·기록) · 기본 모듈 지정 · 사용처 보고 · 발행(서명) · updates · modctl (init lint test bump register add outdated update report verify) | 라이센스 보호 장치 1.0.0을 기본 모듈(onprem)로 등록·발행하고, 나혼자워크·제로라벨·라이센스 서버가 add로 가져와 report까지 |
| 2 | 사용처 검사 결과 · 호환표 · 강제 발행 · 회수 · 알림 · 의심 변경 크기 · check-report | 한쪽에서 1.0.1을 만들어 두 사용처 검사 통과 후 발행, 다른 쪽이 update로 받음 |
| 3 | 기본 모듈 installer·update-agent 등록 · 묶음(기본 모듈 확인 포함) · update_packages 연결 · 공개 묶음 · 카탈로그 · 자동 검사 신호 · 주간 업데이트 PR · new · starter-diff | 고객 서버(시험 PC)에 한 줄 설치 → 묶음으로 업데이트되고 결과가 보고됨 |
14. 대표님이 직접 하실 일
- GitHub에 비공개 저장소
hnbbos/isolo_modules만들기 + 태그 보호 규칙 켜기 - GitHub Packages 사용 설정(npm
@hnbbos범위) - lic.isolo.co.kr에서 연동 키(scope
modules)를 만들어 isolo_modules의 Actions 비밀값LIC_MODULES_KEY로 넣기 - (3단계) 다른 저장소에 신호를 보낼 권한이 좁은 토큰 만들기
- 운영 서버(lic.isolo.co.kr)에 반영