01. 모듈 표준 — 모든 프로그램과 모듈 사이트가 함께 지키는 약속
표준 버전 1 ("moduleStandard": 1) · 작성 2026-10-04 이 문서는 계약입니다. 모듈 사이트(02)는 이 문서와 맞지 않는 모듈을 받지 않고, 각 프로그램(03)은 이 문서대로 모듈을 가져오고 고쳐서 올립니다.
쉬운 요약
- 모듈은 3종류입니다.
- 부품: 코드째 가져다 씁니다.
- 서비스: 한 곳에서 돌리고, 다른 프로그램은 연결해서 씁니다.
- 시작 틀: 새 프로그램을 만들 때 한 번 복사합니다.
- 모듈마다
module.json한 장이 이름표입니다. 무엇인지, 몇 번째 버전인지, 프로그램이 무엇을 연결해 줘야 하는지 적습니다. - 원본은 한 곳(
hnbbos/isolo_modules)에만 둡니다. 프로그램 안의 사본은 고치지 않습니다. - 프로그램은
modules.lock.json에 「어떤 모듈 몇 번을 쓰는지」 적습니다. - 모듈은 프로그램 내부 파일을 직접 가져다 쓰지 않고, 연결부(어댑터)로만 받습니다.
1. 용어
| 용어 | 뜻 |
|---|---|
| 모듈 | 여러 프로그램이 함께 쓰려고 떼어 낸 기능 하나 |
| 사용처 | 그 모듈을 쓰는 프로그램. 라이센스 서버의 제품 코드로 부릅니다(예: nahonza-work) |
| 원본 저장소 | 모듈 소스가 있는 단 하나의 장소. 기본은 hnbbos/isolo_modules |
| 연결부(어댑터) | 모듈이 프로그램에 요구하는 작은 약속(예: 「사용자 찾기」, 「파일 저장」). 사용처가 자기 방식으로 채웁니다 |
| 호스트 연결 파일 | 사용처 안에서 연결부를 채우는 파일. 사용처가 소유하고 고치는 유일한 모듈 관련 파일입니다 |
| 발행 | 검사를 통과한 버전을 「써도 된다」고 확정하고 서명하는 것. 모듈 사이트 관리자가 합니다 |
| 회수 | 발행한 버전에 문제가 있어 「쓰지 말라」고 표시하는 것. 번호는 다시 쓰지 않습니다 |
| 묶음 | 고객 서버에 보낼 「프로그램 버전 + 모듈 버전들 + 이미지 지문」 세트. 서명해서 보냅니다 |
modctl | 모듈 도구(명령줄). 만들기·검사·가져오기·새 버전 받기·보고를 합니다 (02 §11) |
2. 모듈의 3종류
2.1 부품 (part)
- 무엇: 화면이 없거나 작고, 프로그램 안에 들어가 함께 실행되는 코드.
- 예: 라이센스 보호 장치, 설치기, 저장소 추상층(로컬/S3/WebDAV), QR 서명 토큰, 정화(sanitize) 도구.
- 배포
- Node: GitHub Packages의 npm 패키지
@hnbbos/<id> - Python: 원본 저장소의 git 태그
- 언어별 판: 사용처가 Node·Python 양쪽에 있으면 두 판을 만들고, 두 판이 같은 답을 내는지 공통 시험 자료로 확인합니다(§11.2).
- 업데이트 전파: 각 사용처가 새 번호로 올릴 때 반영됩니다.
2.2 서비스 (service)
- 무엇: 프로그램 밖에서 자기 컨테이너로 따로 도는 기능입니다. 어디서 도느냐에 따라 두 가지가 있습니다(
service.placement). - 중앙(
central): 한 곳에서 돌리고 여러 프로그램이 연결합니다. 예: 메신저, 메일. - 곁붙이(
sidecar): 설치마다 프로그램 옆에 함께 돕니다. 예: 문서 변환(docservice), Stirling-PDF, 업데이트 도우미. - 배포:
ghcr.io/hnbbos/<id>이미지(지문@sha256:으로 받음) + 화면 끼워 넣기(iframe·스크립트 위젯·React 컴포넌트) + 약속한 API(openapi.yaml). - 로그인 연결: OAuth 2.1(나혼자워크 인가 서버) 또는 서비스가 발급하는 짧은 토큰.
- 단독 설치: 고객 서버에도 같이 깔려야 하므로
compose.fragment.yml을 제공하고, 묶음(§14)에 들어갑니다. - 업데이트 전파
- 중앙: 서비스를 한 번 고치면 연결된 모든 프로그램에 즉시 반영됩니다. 그래서 API 약속 검사가 가장 중요합니다.
- 곁붙이: 각 설치의 묶음(§14.2)에 들어 있는 이미지 지문이 바뀔 때 반영됩니다.
2.3 시작 틀 (starter)
- 무엇: 새 프로그램을 만들 때 복사하는 뼈대. 프로그램 깊숙이 박히는 것들입니다.
- 예: SaaS/단독 전환(
EDITION), 회사(테넌트) 격리, 운영 콘솔, 설치 마법사, 로그인·권한. - 배포: 원본 저장소
templates/<id>/폴더 + git 태그. - 업데이트 전파: 자동으로는 가지 않습니다. 이미 만든 프로그램은 CHANGELOG의 「달라진 점」을 보고 골라서 반영하고,
modules.lock.json에 어디까지 반영했는지 적습니다.
2.4 어느 종류로 만들까 — 판단 순서
- 프로그램마다 사용자·회사·권한 구조 깊숙이 섞여야 하나? → 시작 틀
- 자기 화면이나 자기 자료(DB 표 여러 개)를 가졌나? → 서비스
- 함수나 미들웨어 수준이고, 사용처마다 작은 연결부만 있으면 되나? → 부품
- 애매하면 서비스를 고릅니다. 한 벌만 유지하면 되기 때문입니다.
2.5 기본 모듈 — 모든 프로그램이 반드시 넣는 것
라이센스 보호·설치·업데이트는 프로그램마다 따로 만들면 안 됩니다. 보호 장치 하나에 구멍이 있으면 그 프로그램만 뚫리고, 고쳐도 다른 프로그램에 전해지지 않습니다. 그래서 이 셋은 모듈 사이트에 올라가는 「기본 모듈」로 정하고, 모든 프로그램이 같은 것을 씁니다.
| id | 종류 | 하는 일 | 지금 상태 (2026-10-04) |
|---|---|---|---|
license-guard | 부품 (Node·Python) | 서명된 라이센스(.lic) 확인 → 정상·유예·잠금 판정, 제품 코드 확인 | 두 벌로 갈라져 있음(제로라벨 268줄 / 라이센스 서버 181줄) → 합쳐서 1.0.0 |
installer | 부품 (shell: install.sh·install.ps1) | 한 줄 설치: Docker 확인 → 묶음 받기 → 비밀값 생성 → .lic 받기 → 실행 → 「설치 완료」 보고 | 없음. 나혼자워크 deploy/standalone/run.sh가 원형 |
update-agent | 서비스 (곁붙이) | 묶음 확인 → 서명·typ 확인 → 백업 → 지문으로 교체 → 상태 확인 → 실패 시 되돌림 → 보고 | 없음. 데스크톱(.exe)용 업데이트 부품만 있음 |
어느 프로그램이 필수인가
- 단독 설치로 파는 제품: 셋 다 필수입니다. 모듈 사이트는 이 셋이 빠진 묶음을 만들지 않습니다(02 §8).
- SaaS로만 운영하는 제품:
license-guard만 권장입니다. 우리 서버이므로 설치기·업데이트 도우미는 필요 없습니다.
라이센스 서버도 사용처입니다. license-guard는 프로그램 쪽(확인하는 쪽)뿐 아니라 라이센스 서버(서명하는 쪽)도 씁니다. 그래서 이 모듈의 약속 검사(§11.2)는 서명·확인 양쪽의 정규화가 바이트 단위로 같은지를 반드시 봅니다.
모듈이 아닌 것
- 라이센스 서버 자체(발급·검증·서명 키 보관): 모듈 사이트가 들어 있는 「본부」입니다.
- 각 프로그램의 고유 업무 기능(견적 계산, 바코드 규격 등)
3. 원본 저장소
기본 원본은 GitHub 비공개 저장소 hnbbos/isolo_modules 하나입니다.
isolo_modules/
├─ modules/ # 부품·서비스
│ ├─ license-guard/
│ └─ messenger/
├─ templates/ # 시작 틀
│ └─ saas-onprem-node/
├─ tools/modctl/ # 모듈 도구 (02 §11)
├─ schemas/module-1.json # module.json 형식 검사용 JSON Schema
└─ .github/workflows/ # 검사·발행 자동화
- 서비스 모듈은 원래 저장소에 둘 수 있습니다. 예를 들어 메신저를 바코드 WMS 저장소에 그대로 두는 경우입니다. 이때
module.json의source.repo·source.path가 원본 위치를 가리키고, 원본은 그 한 곳입니다. - 태그 이름은
<id>-v<version>입니다(예:license-guard-v1.4.0). - 저장소 설정에서 태그 보호 규칙을 켭니다. 발행된 태그를 지우거나 옮길 수 없게 하기 위해서입니다.
- 비공개 저장소이므로 가져올 때 읽기 토큰이 필요합니다. CI 비밀값
MODULES_READ_TOKEN, npm은NODE_AUTH_TOKEN을 씁니다.
4. module.json — 이름표
4.1 공통 칸
| 칸 | 필수 | 설명 |
|---|---|---|
moduleStandard | ✔ | 표준 버전. 지금은 1 |
id | ✔ | 소문자·숫자·하이픈, 2~40자. 한 번 정하면 바꾸지 않음 |
name | ✔ | 한글 이름 |
kind | ✔ | part · service · starter |
version | ✔ | 버전 번호(§7). 예: 1.4.0, 시험판 1.5.0-rc.1 |
summary | ✔ | 한 줄 설명 |
owner | ✔ | 담당(GitHub 사용자 이름) |
source | ✔ | { "repo": "hnbbos/isolo_modules", "path": "modules/license-guard" } |
adapters | 연결부 목록 [{ "name", "required", "summary" }] (§6) | |
config | 설정 칸 [{ "key", "required", "default", "summary" }]. 보안 판단에 쓰는 값은 넣지 않음(§10-1) | |
requires | { "modules": { "<id>": "1.x" }, "host": { "node": ">=20", "python": ">=3.11", "db": ["mariadb","postgres"] } } | |
provides | 사용처에 생기는 것 { "routes": [], "tables": [], "events": [], "ui": [] } | |
network | 이 모듈이 바깥으로 접속하는 주소 목록. 예: ["lic.isolo.co.kr"] | |
tenancy | ✔ | multi(회사별 격리 지원) · single(한 회사 전제) · none(회사와 무관) |
docs | ✔ | { "integration": "INTEGRATION.md", "changelog": "CHANGELOG.md" } |
catalog | 고객 패키지 센터 노출(서비스만). { "visible": true, "category": "업무", "icon": "message-circle", "tier": "licensed", "featureKey": "messenger", "baseUrl": "…" }. tier는 free 또는 licensed |
4.2 부품 전용 — runtimes
"runtimes": {
"node": { "package": "@hnbbos/license-guard", "path": "node", "format": "esm+cjs", "engines": ">=20" },
"python": { "package": "isolo-license-guard", "path": "python", "requires": ">=3.11" }
}
format이 esm+cjs이면 import와 require를 모두 지원해야 합니다. 나혼자워크는 ESM, 제로라벨·라이센스 서버는 CommonJS이기 때문입니다.
설치기처럼 고객 서버에서 도는 스크립트는 shell 판을 둡니다. 리눅스·NAS와 윈도우를 모두 지원해야 하므로 두 파일이 필요합니다.
"runtimes": {
"shell": { "path": "shell", "files": ["install.sh", "install.ps1"] }
}
4.3 서비스 전용 — service
"service": {
"placement": "central",
"image": "ghcr.io/hnbbos/messenger",
"port": 8080,
"health": "/health",
"compose": "service/compose.fragment.yml",
"api": "service/openapi.yaml",
"auth": "oauth2",
"embed": { "iframe": "/embed/messenger", "script": "/widget.js", "react": "@hnbbos/messenger-react" },
"data": "own-db"
}
placement는 central(중앙) 또는 sidecar(곁붙이)입니다(§2.2). data는 own-db(서비스가 자기 DB를 가짐) 또는 host-db(사용처 DB에 표를 만듦, §9) 중 하나입니다. 서비스는 own-db를 권합니다.
4.4 시작 틀 전용 — starter
"starter": {
"stack": "node-ts-express-prisma-react",
"path": "template",
"variables": ["PRODUCT_CODE", "PRODUCT_NAME", "PORT"],
"includes": { "license-guard": "1.x" }
}
4.5 예시 — 라이센스 보호 장치
{
"moduleStandard": 1,
"id": "license-guard",
"name": "라이센스 보호 장치",
"kind": "part",
"version": "1.0.0",
"summary": "서명된 라이센스(.lic)를 확인해 정상·유예·잠금 상태를 정한다",
"owner": "hnbbos",
"source": { "repo": "hnbbos/isolo_modules", "path": "modules/license-guard" },
"tenancy": "none",
"runtimes": {
"node": { "package": "@hnbbos/license-guard", "path": "node", "format": "esm+cjs", "engines": ">=20" },
"python": { "package": "isolo-license-guard", "path": "python", "requires": ">=3.11" }
},
"adapters": [
{ "name": "SettingsStore", "required": true, "summary": "설치된 .lic 와 마지막 확인 시각 저장" },
{ "name": "AuditLog", "required": false, "summary": "상태가 바뀔 때 기록" },
{ "name": "Clock", "required": false, "summary": "현재 시각(검사에서 고정용)" }
],
"config": [
{ "key": "productCode", "required": true, "summary": "이 프로그램의 제품 코드(코드에 고정해 넘김 — 환경변수 금지)" },
{ "key": "graceDays", "required": false, "default": 14, "summary": "만료 전 경고 시작 일수" }
],
"network": ["lic.isolo.co.kr"],
"docs": { "integration": "INTEGRATION.md", "changelog": "CHANGELOG.md" }
}
5. 폴더 모양
5.1 부품
modules/license-guard/
├─ module.json
├─ INTEGRATION.md # 붙이기 안내서 (§12)
├─ CHANGELOG.md # 변경 기록 (§13)
├─ node/
│ ├─ package.json # name = @hnbbos/license-guard, version = module.json 과 같게
│ ├─ src/index.ts # 바깥에 내보내는 것은 여기서만
│ ├─ src/adapters.ts # 연결부 약속(interface)
│ └─ test/
├─ python/
│ ├─ pyproject.toml # version = module.json 과 같게
│ ├─ isolo_license_guard/__init__.py
│ ├─ isolo_license_guard/adapters.py # 연결부 약속(Protocol)
│ └─ tests/
├─ contract/cases.json # 두 판이 같은 답을 내는지 보는 공통 시험 자료 (§11.2)
└─ examples/ # 가장 작은 사용 예 (Express 하나, FastAPI 하나)
5.2 서비스
modules/messenger/
├─ module.json · INTEGRATION.md · CHANGELOG.md
├─ service/
│ ├─ Dockerfile
│ ├─ compose.fragment.yml # 단독 설치 묶음에 끼울 조각
│ ├─ openapi.yaml # 바깥에 약속한 API
│ └─ src/
├─ widget/ # iframe 진입 · 스크립트 위젯 · React 컴포넌트
└─ test/ # API 약속 검사 · 회사 격리 검사
5.3 시작 틀
templates/saas-onprem-node/
├─ module.json · INTEGRATION.md(새 프로그램 만들기 안내) · CHANGELOG.md
└─ template/ # {{PRODUCT_CODE}} 같은 자리표시자 포함
버전 번호는 module.json이 기준입니다. package.json·pyproject.toml의 번호가 다르면 modctl lint가 실패합니다.
6. 연결부(어댑터) 규칙
- 모듈은 사용처 파일을 import하지 않습니다. 필요한 것은 모두 연결부로 받습니다.
- 진입점은 하나입니다.
configure({ config, adapters })가 모듈 객체(함수·라우터·미들웨어)를 돌려줍니다. 전역 상태로 설정하지 않습니다. - 연결부는 작게 만듭니다. 메서드 3~7개. 이름은 하는 일로 짓습니다.
- 표준 연결부 이름: 여러 모듈이 같은 이름을 쓰면, 사용처는 한 번 만든 연결부를 다른 모듈에도 재사용합니다.
| 이름 | 하는 일 | 비고 |
|---|---|---|
TenantResolver | 요청 → 회사 ID | 한 회사용 프로그램은 상수를 돌려줌 |
UserProvider | 사용자 조회·목록·시스템 사용자 | 바코드 메신저와 같은 이름 |
FileStorage | 저장·읽기·삭제·존재 확인 | 바코드 메신저와 같은 이름. 나혼자워크 StorageProvider와 맞춤 |
Notifier | 사용자에게 알림 | 바코드 메신저와 같은 이름 |
SettingsStore | 키-값 설정(회사별 또는 배포 전체) | |
AuditLog | 중요한 일 기록 | |
Clock | 현재 시각 | 검사에서 시간을 고정할 때 |
HttpGuard | 바깥 주소 접속 전 사설망·금지 주소 검사 | 사용자가 입력한 주소로 접속하는 모듈은 필수 |
- 연결부 약속이 바뀌면 MAJOR입니다(§7).
- 연결부 메서드가 실패하면 모듈은 삼키지 않고 오류를 위로 올립니다. 「없음」과 「물어보지 못함」을 구분해야 합니다. 나혼자워크 저장소 작업에서 이 둘을 섞어 자료 유실 위험이 생겼던 일이 있습니다.
6.1 예 — Node(TypeScript)
// modules/license-guard/node/src/adapters.ts
export interface SettingsStore {
get(key: string): Promise<string | null>; // 없으면 null, 못 읽으면 throw
set(key: string, value: string): Promise<void>;
}
export interface AuditLog { record(action: string, detail: Record<string, unknown>): Promise<void>; }
export interface GuardAdapters { settings: SettingsStore; audit?: AuditLog; clock?: () => Date; }
export interface GuardConfig { productCode: string; graceDays?: number; }
// modules/license-guard/node/src/index.ts
export function configure(opts: { config: GuardConfig; adapters: GuardAdapters }): LicenseGuard { /* … */ }
6.2 예 — Python
# modules/license-guard/python/isolo_license_guard/adapters.py
from typing import Protocol
class SettingsStore(Protocol):
def get(self, key: str) -> str | None: ... # 없으면 None, 못 읽으면 예외
def set(self, key: str, value: str) -> None: ...
6.3 사용처 쪽 — 호스트 연결 파일
// (나혼자워크) server/src/modules/license-guard.host.ts — 사용처가 소유하는 유일한 파일
import { configure } from '@hnbbos/license-guard';
// 예시: 「배포 전체」 설정 저장 함수. 회사별·사용자별 표(나혼자워크 AppSetting 은 사용자별)에 두면 안 된다.
// 나혼자워크라면 LicenseSetting 같은 한 줄짜리(singleton) 표가 맞는 자리다.
import { readDeploymentSetting, writeDeploymentSetting } from '../services/deployment-settings.js';
export const guard = configure({
config: { productCode: 'nahonza-work' }, // 코드에 고정 — 환경변수로 받지 않음
adapters: {
settings: {
get: (k) => readDeploymentSetting(`license-guard:${k}`), // 없으면 null, DB 오류는 그대로 throw
set: (k, v) => writeDeploymentSetting(`license-guard:${k}`, v),
},
},
});
7. 버전 규칙
| 바뀐 것 | 번호 | 사용처는 |
|---|---|---|
| 버그·보안 고침. 겉으로 보이는 동작은 같음 | PATCH 1.4.0 → 1.4.1 | 자동으로 받아도 안전 |
| 기능 추가. 기존 사용법은 그대로 동작 | MINOR 1.4.1 → 1.5.0 | 골라서 받기 |
| 아래 「MAJOR 목록」 중 하나라도 해당 | MAJOR 1.5.0 → 2.0.0 | 안내서대로 손보고 받기 |
MAJOR 목록
- 연결부 메서드 추가(필수)·삭제·모양 변경
- 설정 칸 삭제, 이름 변경, 의미 변경, 기본값 변경으로 동작이 달라짐
- API 응답에서 칸 삭제·이름 변경, 주소 변경
- DB 표·칸 삭제·이름 변경(§9 「축소」)
- 최소 런타임 올림(예: Node 20 → 22)
- 화면 끼워 넣기 방식 변경(iframe 주소, 위젯 옵션 이름)
규칙
- 시험판은
1.5.0-rc.1처럼 붙입니다. 시험판은 발행되지 않고 검사에만 씁니다. - 한 번 쓴 번호는 다시 쓰지 않습니다. 회수한 번호도 마찬가지입니다.
- 새 번호는 같은 MAJOR 줄에서 가장 높은 번호보다 커야 합니다. 다만 옛 줄에 보안 고침을 내는 것(예:
2.0.0이 나온 뒤1.4.2)은 허용합니다. - 사용처는 정확한 번호로 고정합니다.
"@hnbbos/license-guard": "1.4.0"처럼 쓰고,^1.4.0같은 범위는 금지입니다. 범위를 쓰면 아직 발행되지 않은(검사 중인) 번호가 저절로 설치될 수 있습니다. 번호 올리기는modctl update만 합니다.
8. 사용처 기록 — modules.lock.json
각 프로그램 저장소의 최상위에 둡니다. modctl이 쓰고 읽으며, 모듈 사이트에 보고됩니다.
{
"lockVersion": 1,
"product": "nahonza-work",
"modules": {
"license-guard": {
"kind": "part", "version": "1.4.0", "runtime": "node",
"host": "server/src/modules/license-guard.host.ts"
},
"messenger": {
"kind": "service", "version": "2.4.0",
"image": "ghcr.io/hnbbos/messenger@sha256:…", "embed": "widget"
},
"saas-onprem-node": {
"kind": "starter", "version": "1.0.0", "appliedAt": "2026-10-20",
"pending": ["1.1.0 설치 마법사 개선 — 아직 반영 안 함"]
}
},
"patches": [
{
"module": "license-guard",
"reason": "만료일 계산 오류 긴급 우회",
"issue": "https://github.com/hnbbos/isolo_modules/issues/12",
"removeBy": "2026-11-01"
}
]
}
patches는 임시 덧댐을 적는 곳입니다(03 작업 D-긴급). 적지 않은 덧댐은 금지입니다.removeBy가 지나면modctl outdated가 경고합니다.
9. 자료(DB)를 가진 모듈
- 서비스는 자기 DB를 갖는 것(
own-db)이 기본입니다. 사용처 DB에 표를 만드는 것(host-db)은 부품이나 작은 서비스에만 씁니다. - 새 모듈의 표 이름에는 접두사를 붙입니다(
<id 줄임>_…). 이미 있는 모듈(예: 바코드 메신저chat_*)은 그대로 두고provides.tables에 적습니다. tenancy: "multi"인 모듈의 표는 모두 회사 칸(tenant_id)을 가지며, 모든 조회에 회사 조건이 붙습니다. 한 회사용 프로그램은TenantResolver가 상수를 돌려줍니다.- DB 변경 파일은
data/migrations/0001_<이름>.sql(MariaDB/MySQL)로 둡니다. PostgreSQL을 지원하면.pg.sql도 둡니다. - 여러 번 돌려도 결과가 같아야 합니다(
IF NOT EXISTS). - 앞으로만 갑니다. 되돌리기는 「이전 번호의 코드가 새 표에서도 도는 것」으로 보장합니다.
- 지우기·이름 바꾸기는 확장 → 옮기기 → 축소의 세 번 발행으로 나누고, 축소는 MAJOR입니다.
- 사용처의 ORM 모델은 사용처가 소유합니다. 모듈은 붙여 넣을 조각을 제공합니다.
- Prisma(나혼자워크):
data/prisma.fragment.prisma - SQLAlchemy(바코드 WMS):
data/sqlalchemy_models.py - 모듈은 사용처의 표를 고치지 않습니다(
ALTER·직접 조회 금지). 사용처 자료가 필요하면 연결부로 받습니다.
10. 보안 규칙 — 반드시
- 보안 판단을 환경변수로 바꿀 수 없게 합니다. 검증용 공개키, 강제 여부, 제품 코드, 권한 판정이 여기에 해당합니다.
- 단독 설치에서는 환경변수를 고객이 쥐고 있습니다. 실제로 기존 라이센스 보호 장치는 공개키·강제 여부를 환경변수(
VENDOR_LICENSE_ENFORCE등)로 받아 우회될 수 있었습니다. - 환경변수로 받아도 되는 것: 주소, 포트, 성능 값, 기록 수준.
- 비밀값을 모듈에 넣지 않습니다. 예제·시험 자료에도 진짜 값은 넣지 않습니다.
- 바깥 접속은
network에 적은 곳만 합니다. 사용자가 입력한 주소로 접속하는 모듈은HttpGuard로 사설망·금지 주소를 막습니다(SSRF). - 회사 격리: 모든 조회에 회사 조건을 붙이고, 검사에 「다른 회사 자료가 보이지 않는다」 항목을 반드시 넣습니다.
- 사용자가 입력한 HTML은 정화합니다. 파일 경로는
FileStorage연결부로만 다룹니다(직접 경로 조립 금지). - 기록(로그)에 비밀값·토큰·개인정보를 남기지 않습니다.
- 의존 패키지는 잠금 파일로 고정하고, 발행 전에 취약점 검사(
npm audit,pip-audit)를 통과합니다. - 화면 모듈은 사용처의 전역 스타일(Tailwind 등)에 기대지 않습니다. 접두사를 붙인 CSS나 CSS 변수를 씁니다.
- iframe 끼워 넣기는 반드시 다른 원본(주소)에서 엽니다. 같은 원본에
allow-scripts allow-same-origin을 주면 끼워 넣은 화면이 부모 창의 로그인 정보에 닿습니다(나혼자워크integration.controller.ts주석에 기록된 문제). - 서비스의 공개 경로(인증 없이 열리는 주소)는
openapi.yaml에security: []로 명시하고, 검사에 「인증 없이 막혀야 할 곳이 막히는지」를 넣습니다.
11. 검사 규칙
11.1 세 겹의 검사
| 검사 | 누가 | 무엇 |
|---|---|---|
| 단위 검사 | 모듈 저장소 | 가짜 연결부로 모듈 혼자 돌림 |
| 약속 검사 | 모듈 저장소 | 두 판(Node·Python)이 같은 답을 내는지(§11.2), 서비스는 openapi.yaml대로 답하는지 |
| 사용처 검사 | 각 프로그램 저장소 | 자기 실제 연결부로 새 번호를 넣고 원래 검사를 돌림 → 결과를 모듈 사이트에 보고 |
11.2 공통 시험 자료 — contract/cases.json
두 판이 같은 입력에 같은 출력을 내야 하는 곳(서명 정규화, 상태 판정, 날짜 계산)은 시험 자료를 공유합니다. 특히 서명용 JSON 정규화(canonical)는 서명하는 쪽(라이센스 서버)과 확인하는 쪽이 바이트 단위로 같아야 합니다.
[
{ "name": "만료 10일 전 → 유예", "fn": "vendorState",
"input": { "payload": { "expiresAt": "2026-10-14" }, "now": "2026-10-04T00:00:00Z" },
"expect": { "state": "grace" } }
]
11.3 검사가 「정말 돌았는지」 확인
- 결과 줄이 없으면 실패로 칩니다. 도중에 죽은 검사를 「이상 없음」으로 읽으면 안 됩니다.
- 검사 시작 시 지금 도는 코드의 모듈 번호를 출력하고 기대한 번호와 대조합니다. 낡은 이미지나 낡은 설치본으로 검사가 통과하는 일을 막기 위해서입니다.
11.4 사용처 검사 결과 보고 형식
{
"module": "license-guard", "version": "1.5.0",
"product": "nahonza-work", "productVersion": "1.8.0", "runtime": "node",
"passed": 42, "failed": 0, "ok": true,
"logUrl": "https://github.com/hnbbos/alone_boss/actions/runs/…"
}
12. 붙이기 안내서(INTEGRATION.md) 목차 — 필수
- 이 모듈이 하는 일(3줄)
- 먼저 판단할 것: 소스를 가져와야 하나, 서비스 연결로 충분한가
- 준비물: 런타임, DB, 함께 필요한 모듈
- 가져오기: 언어별 명령(
modctl add …) - 연결부 채우기: 연결부마다 메서드 설명 + Node·Python 예제
- 설정 칸: 값과 기본값. 보안 판단은 왜 코드에 고정하는지
- 화면 붙이기(있으면): iframe / 스크립트 위젯 / React
- 자료(DB) 준비(있으면)
- 확인하는 법: 사용처 검사에 꼭 넣을 최소 항목
- 자주 막히는 곳
- Claude 요청문(복사용): 「이 프로그램에 ○○ 모듈을 붙여 줘 …」
13. 변경 기록(CHANGELOG.md) 형식
## 1.5.0 — 2026-10-20
### 새 기능
- 만료 전 경고 시작일을 설정으로 정할 수 있음(`graceDays`)
### 고침
- 윤년 2/29 만료일이 하루 일찍 잠기던 것
### 사용처가 할 일
- 없음 (graceDays 를 안 넣으면 지금처럼 14일)
「사용처가 할 일」은 빼지 않습니다. 할 일이 없으면 「없음」이라고 씁니다. modctl update가 이 부분을 그대로 보여 줍니다.
14. 서명 — 발행 기록과 묶음
모든 서명은 라이센스 서버의 Ed25519 키링으로 합니다. 개인키는 라이센스 서버 밖으로 나가지 않습니다(VENDOR-KEY-RUNBOOK.md). 정규화는 라이센스 보호 장치의 canonical()(정렬 키 JSON)을 그대로 씁니다.
14.1 발행 기록
{
"typ": "isolo.module-release/1",
"id": "license-guard",
"version": "1.5.0",
"manifestSha256": "…",
"artifacts": {
"npm": { "name": "@hnbbos/license-guard", "version": "1.5.0", "integrity": "sha512-…" },
"git": { "repo": "hnbbos/isolo_modules", "tag": "license-guard-v1.5.0", "commit": "<40자리>" },
"image": "ghcr.io/hnbbos/messenger@sha256:…"
},
"releasedAt": "2026-10-20T03:00:00Z"
}
14.2 묶음 — 고객 서버로 보내는 세트
{
"typ": "isolo.bundle/1",
"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", "messenger": "2.4.0" },
"createdAt": "2026-10-21T01:00:00Z"
}
14.3 확인하는 쪽의 규칙
typ부터 확인합니다. 같은 키로.lic도 서명하므로, 종류가 다른 서명본을 다른 용도로 쓰지 못하게 해야 합니다.- 공개키는 코드에 내장합니다(환경변수 금지, §10-1). 키 교체는 키링에 새 키를 추가해 배포하는 방식입니다.
- 이미지는 이름표(
:latest)가 아니라 지문(@sha256:)으로 받습니다.
15. 이 표준을 바꿀 때
- 이 문서를 고치고
moduleStandard번호를 올립니다. - 모듈 사이트는 바뀌는 동안 이전 번호와 새 번호를 함께 받습니다.
- 바뀐 점과 이유를 아래에 적습니다.
| 표준 | 날짜 | 바뀐 점 |
|---|---|---|
| 1 | 2026-10-04 | 처음 작성 |