Skip to content

feat: self-hosted 모드 (#41) — 내 NAS/PC + 공인 IP로 발행 - #42

Merged
jkas2016 merged 32 commits into
mainfrom
feat/41-self-hosted-mode
Jul 4, 2026
Merged

feat: self-hosted 모드 (#41) — 내 NAS/PC + 공인 IP로 발행#42
jkas2016 merged 32 commits into
mainfrom
feat/41-self-hosted-mode

Conversation

@jkas2016

@jkas2016 jkas2016 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Closes #41

내 NAS/PC + 공인 IP로 publish → 지속 링크 UX를 제공하는 세 번째 호스팅 모드 self-hosted. 이슈 #41 전체(Phase 1~3)를 관리하는 umbrella PR이며, 각 Phase를 이 브랜치(feat/41-self-hosted-mode)에 이어 쌓아 한 번에 머지한다.

Spec: docs/superpowers/specs/2026-07-03-self-hosted-mode-design.md
Plan: docs/superpowers/plans/2026-07-03-self-hosted-mode.md

진행 현황

✅ Phase 1 — self-hosted 모드 (MVP, 기존 웹서버 가정) — 완료

  • 스토리지 백엔드 추상화 (src/lib/backend.ts): StorageBackend + S3Backend(aws.ts 무수정 위임) + LocalFsBackend
  • publish/list/rm 백엔드 리팩터 (현행 두 모드 무회귀)
  • resolveConfig self-hosted 파생(serveRoot 판별자) + host/port/scheme + 모호성 가드
  • buildPublicUrl self-hosted 분기 + 설정 host 우선 / 공인 IP 자동감지(+경고) — ensureHost로 비동기 분리
  • _meta 차단 + index rewrite 스니펫(Caddy 기본 / nginx 옵션)
  • hostdoc setup --serve-root (config 저장 + 스니펫 출력, 자동설치 없음)

⬜ Phase 2 — hostdoc setup이 웹서버 설치·구동 자동

  • Caddy 정적 바이너리 다운로드·설치 (플랫폼 감지)
  • Caddyfile 생성기 (serve root + /_* 차단 + index rewrite + 도메인 시 자동 HTTPS)
  • 구동 + OS 서비스 등록(재부팅 생존), deprovision으로 중지·제거
  • setup 시 보안 경고 및 노출 안내

🚧 Phase 3 — 플랫폼 확장 & 편의 (Windows 완료)

  • Synology/QNAP NAS 패키징·서비스 통합
  • Windows 서비스 통합
  • DDNS 자동 갱신 헬퍼(선택)

🔧 Phase 1 후속 정리 (최종 리뷰 Minor — non-blocking)

  • env 입력 검증: HOSTDOC_PORT NaN 가드 + HOSTDOC_SCHEME 화이트리스트 (src/lib/config.ts)
  • 공유 parsePort 헬퍼로 src/index.ts port 파싱 중복 + --port 0 엣지 해소
  • LocalFsBackend 인접 prefix 테스트 보강 (test/backend.test.ts)
  • LocalFsBackend.list/exists 전체 walk → prefix 서브트리 최적화 (Phase 2 병행)

Non-goals

포트포워딩/방화벽 자동화 · 베어 IP HTTPS 보장(호스트명 있을 때만).

테스트 (Phase 1 기준)

257/257 통과, typecheck(tsgo)·build 클린. CI는 AWS 크레덴셜·Terraform 없이 그대로 통과. 신규: backend/config/url/host/snippet/setup-selfhosted + self-hosted E2E(publish→list→rm).

리뷰 (Phase 1)

TDD 8 태스크, 태스크별 spec+quality 리뷰 통과 후 전체-브랜치 리뷰 Ready to merge: Yes (0 Critical/0 Important). Phase 2/3 완료 후 재리뷰하고 머지한다.

🤖 Generated with Claude Code

jkas2016 and others added 11 commits July 3, 2026 15:32
Storage backend abstraction (S3Backend/LocalFsBackend), self-hosted
mode derivation via serveRoot, host resolution (config host > public-IP
auto-detect + warning), _meta/index snippet (Caddy default, nginx opt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hange)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ppet)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jkas2016 jkas2016 added enhancement New feature or request priority: low Low priority labels Jul 3, 2026
@jkas2016 jkas2016 changed the title feat: self-hosted 모드 (Phase 1 MVP) — 내 NAS/PC + 공인 IP로 발행 (#41) feat: self-hosted 모드 (#41) — 내 NAS/PC + 공인 IP로 발행 Jul 3, 2026
@jkas2016
jkas2016 marked this pull request as draft July 3, 2026 07:36
jkas2016 and others added 9 commits July 3, 2026 17:08
…service)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route deprovision on resolveConfig({}).mode so HOSTDOC_SERVE_ROOT env config
is honored (was reading loadConfig().serveRoot, missing the env override).
Also: symmetric removeService darwin exists-guard, dedup security warning,
drop dead test imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jkas2016

jkas2016 commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Phase 2 완료 — setup --install 자동 설치 (커밋 49f1447..71774b2)

Phase 1(기존 웹서버 가정)에 이어 웹서버 자동 설치·구동·서비스 등록을 추가했습니다.

  • hostdoc setup --serve-root <p> --install → Caddy 확보(PATH 재사용 / 공식 다운로드, 플랫폼 감지) → Caddyfile 기록(Phase 1 caddySnippet 재사용, _meta 차단·index rewrite·도메인 시 auto-HTTPS) → OS 시스템 서비스 등록(macOS launchd / Linux systemd)로 재부팅 생존.
  • 권한: 비특권 준비는 유저로, 서비스 등록만 root — root이 아니면 정확한 sudo 명령을 출력하고 중단(hostdoc이 임의 sudo 실행 안 함).
  • hostdoc deprovision → self-hosted 감지 시 서비스만 중지·해제(발행 파일·Caddy 바이너리 보존), cloud는 기존 terraform destroy.
  • nginx는 자동설치 대상 아님(단일 정적 바이너리 아님) → 스니펫 수동 유지.

구조

  • 순수 생성기(platform.ts/service.ts) + I/O 경계(installer.ts, 주입형 Deps) 분리 → CI는 무네트워크·무root로 통과(terraform.ts shell-out 패턴과 동형).
  • 4개 동결 파일(aws.ts/cloudfront.ts/terraform.ts/snippet.ts) 무수정 — Phase 1·클라우드 모드 무회귀.

검증

  • 286/286 테스트, typecheck·build 클린.
  • 6태스크 TDD + 태스크별 spec/quality 리뷰 + 최종 whole-branch 리뷰(opus). Important 1건(deprovision을 loadConfig 대신 resolveConfig 모드 파생으로 교체 — HOSTDOC_SERVE_ROOT env 구성 오라우팅 수정) + Minor 3건 수정 완료.
  • 공식 문서 검증: systemd unit(caddyserver/dist), 다운로드 API(?os=&arch=), launchctl bootstrap/bootout(macOS 11+).

Spec/Plan: docs/superpowers/specs/2026-07-03-self-hosted-mode-phase2-design.md, docs/superpowers/plans/2026-07-03-self-hosted-mode-phase2.md

jkas2016 and others added 4 commits July 4, 2026 23:57
Windows arm of Phase 3: extend `setup --install` to register Caddy as a
boot-surviving Windows service via WinSW (caddy has no native SCM stub —
caddyserver/caddy#4670 PR unmerged, so sc.exe risks error 1053). Mirrors
Phase 2's launchd/systemd installer seam; renames sudoCommands →
privilegedCommands (platform-neutral). x64 first; arm64 TBD at impl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WinSW-based Windows service integration for setup --install / deprovision.
Verified constants: WinSW v2.12.0 WinSW-x64.exe (no arm64 asset), caddy
windows download is a bare .exe. Tasks: platform detect, winswXml, sudo→
privileged rename, windows leaf helpers, windows service control, command
routing + platform-aware CLI headers. All I/O behind injected seams; CI
verifies windows paths on Linux via explicit plat injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jkas2016 and others added 4 commits July 5, 2026 00:28
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jkas2016

jkas2016 commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

Phase 3 (Windows) 완료 — setup --install이 Windows 서비스 등록 (커밋 49b3b18..c4846f8)

Phase 2(macOS launchd / Linux systemd)에 이어 Windows 서비스 통합을 추가했습니다. NAS 패키징·DDNS는 미완으로 유지.

  • hostdoc setup --serve-root <p> --install이 Windows(x64)에서 caddy.exe 확보(PATH 재사용 / 공식 다운로드) → WinSW 래퍼(caddy-service.exe) 확보 → Caddyfile + caddy-service.xml 기록 → WinSW로 Windows 서비스 등록·구동(재부팅 생존).
  • 왜 WinSW인가: Caddy는 네이티브 Windows 서비스 스텁이 없음(Minimal windows service stub caddyserver/caddy#4670 PR 미병합) → sc.exe로 감싼 caddy run은 SCM 미응답으로 error 1053 위험. 공식 문서(running)·커뮤니티가 권장하는 WinSW로 확정.
  • 권한: Windows엔 sudo가 없어 net session 프로브로 관리자 판정 → 비관리자면 정확한 명령을 출력하고 중단("Open an elevated (Administrator) PowerShell and run: …"). 임의 권한 상승 안 함(Unix sudo human-gate와 대칭).
  • deprovision: Windows self-hosted 감지 시 WinSW stop+uninstall + caddy-service.xml·Caddyfile 제거. 발행 파일·caddy.exe·WinSW 바이너리는 보존.

구조

  • Phase 2의 주입 seam을 그대로 확장 — 순수 생성기(platform.ts win32 분기 + winswDownloadUrl / service.ts winswXml)와 I/O 경계(installer.tsserviceDir/downloadWinsw/windows 서비스 제어/isPrivileged) 분리. CI(Linux)는 무네트워크·무관리자로 통과(plat 명시 주입으로 Windows 경로 검증).
  • 플랫폼 중립 개명: sudoCommands/needs-rootprivilegedCommands/needs-privilege(Unix 동작 무회귀, 순수 개명).
  • 동결 4파일(aws.ts/cloudfront.ts/terraform.ts/snippet.ts) 무수정 — Phase 1·2·클라우드 모드 무회귀.

검증

  • CI(Linux) 전 스위트 통과(신규 Windows 케이스 포함), typecheck(tsgo)·build 클린.
  • 6태스크 TDD + 태스크별 spec/quality 리뷰 + 최종 whole-branch 리뷰(opus) Ready to merge: Yes(0 Critical / 0 Important). 잔여는 전부 Minor(비차단): winswXml XML-이스케이프(기존 launchd/systemd와 동일 패턴 — 3개 생성기 일괄 하드닝을 별도 태스크로), "sudo"→"privileged" 잔여 문구 정리.
  • 공식 문서/소스 검증: WinSW v2.12.0 WinSW-x64.exe(v2.x는 arm64 자산 없음 → x64 에뮬), Caddy Windows 다운로드가 단일 .exe, WinSW v2 stop/uninstall 멱등성(Program.cs).

Spec/Plan: docs/superpowers/specs/2026-07-04-self-hosted-mode-phase3-windows-design.md, docs/superpowers/plans/2026-07-04-self-hosted-mode-phase3-windows.md

jkas2016 and others added 2 commits July 5, 2026 02:00
On a Windows checkout (core.autocrlf=true, no .gitattributes) the shebang
.mjs wrapper scripts get CRLF line endings. esbuild/vite's hashbang
stripping then leaves an invalid token, so importing them from
test/skill.test.ts fails the whole suite with "SyntaxError: Invalid or
unexpected token" (CI on Linux is LF, so it never saw this). A CRLF shebang
also breaks execution on Unix. Add `*.mjs text eol=lf` so these scripts are
always LF regardless of the contributor's autocrlf.

Once the suite collected, a second latent CRLF assumption surfaced: the
SKILL.md frontmatter regex hard-coded `\n`, so it failed on the CRLF
checkout. Make it line-ending agnostic (`\r?\n`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test/pack.test.ts spawned npm with execFileSync("npm", [...]), which throws
spawnSync ENOENT on Windows (the launcher is npm.cmd, and execFileSync does
not apply PATHEXT). Spawning npm.cmd directly is EINVAL since Node's
CVE-2024-27980 fix, and shell:true with an args array warns DEP0190. Use
execSync with a fixed literal command string: the shell resolves npm.cmd on
Windows and /bin/sh runs it on Linux/CI, with no deprecation noise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jkas2016

jkas2016 commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

부수: Windows 개발 환경 테스트 실패 2건 수정 (커밋 cdb7b92·e4f264f)

Phase 3와 무관한 사전 존재 실패(Linux CI는 원래 green, Windows 체크아웃에서만 발생)를 이 브랜치에 함께 정리했습니다.

  • test/skill.test.ts 수집 실패 (SyntaxError: Invalid or unexpected token): core.autocrlf=true + .gitattributes 부재로 shebang .mjs 래퍼가 CRLF로 체크아웃 → esbuild/vite의 hashbang 제거가 깨짐(통제 재현: shebang+CRLF만 실패). CRLF shebang은 Unix 실행도 깨뜨림. *.mjs text eol=lf로 LF 강제. 이후 드러난 2차 잠복 버그(SKILL.md 프론트매터 정규식이 \n 하드코딩)는 \r?\n로 줄끝 무관하게 수정.
  • test/pack.test.ts spawnSync npm ENOENT: execFileSync("npm", …)가 Windows에서 npm.cmd를 못 찾음(직접 npm.cmd 실행은 CVE-2024-27980 이후 EINVAL, shell:true+args는 DEP0190 경고). 고정 리터럴 문자열 execSync로 교체 → 셸이 Windows는 npm.cmd, Linux는 /bin/sh로 해석, 경고 없음.

모두 크로스플랫폼 안전(Linux/CI 동작 불변). Windows에서 전체 스위트 302 pass / 3 skip / 0 fail.

Real end-to-end testing on Windows surfaced a deprovision ordering bug the
injected-fake unit tests could not: runDeprovisionSelfHosted removed the
staged caddy-service.xml first, then called removeService (WinSW
stop/uninstall). But WinSW loads caddy-service.[xml|yml] for EVERY command,
so `caddy-service.exe stop` aborted with "Unable to locate
caddy-service.[xml|yml]" and the service was left installed and running.

On Windows, tear the service down before removing the xml/Caddyfile; when
not elevated, leave the files in place so the stop/uninstall commands we
hand the user still find the xml. Unix ordering is unchanged (the staged
unit is a copy; the live service file lives at systemServicePath).

New tests assert the invariant: the xml exists while WinSW stop/uninstall
runs (admin), and is not pre-deleted when not admin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jkas2016

jkas2016 commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

실제 Windows 실행 테스트로 deprovision 버그 발견·수정 (커밋 cff6bc8)

관리자 권한으로 Windows에서 setup --install → 실서비스 등록 → 서빙 검증 → deprovision까지 실제 실행하며 유닛 테스트(주입 fake)로는 안 잡히던 순서 버그를 발견해 고쳤습니다.

실측 검증 (real machine):

  • setup --install: WinSW가 hostdoc-caddy 서비스 등록·구동(StartType=Automatic, 재부팅 생존), caddy.exe·WinSW·caddy-service.xml 서비스 디렉토리 배치.
  • 서빙: GET /<code>/200 + 발행 HTML, GET /_meta/…403, 확장자 없는 경로 → index rewrite 200. (loopback host라 Caddy가 내부 CA로 자동 HTTPS)

버그 & 수정:

  • runDeprovisionSelfHostedcaddy-service.xml먼저 삭제한 뒤 WinSW stop/uninstall을 호출 → WinSW는 모든 명령에 자기 xml이 필요(Unable to locate caddy-service.[xml|yml]) → 서비스가 안 지워지고 계속 Running.
  • Windows에서는 teardown(stop+uninstall)을 xml 삭제보다 먼저 수행하도록 순서 교정. 비관리자면 xml을 남겨 사용자가 실행할 stop/uninstall 명령이 유효하게 함. Unix 순서는 불변(스테이징 유닛은 복사본).
  • 불변식을 고정하는 테스트 추가(WinSW 명령 실행 중 xml 존재 / 비관리자 시 xml 미삭제).
  • 수정 후 실측 재검증: deprovision이 stop→uninstall 성공, 서비스 REMOVED, 포트 free, caddy 프로세스 0.

전체 스위트 303 pass / 3 skip, CI green. 실제 실행 테스트가 아니었으면 놓쳤을 버그입니다.

buildPublicUrl returns http:// by default (cfg.scheme ?? "http"), but
caddySnippet emitted a bare site address (host / host:port), which Caddy
treats as a signal to enable Automatic HTTPS for any hostname or IP (per
Caddy's docs). Real Windows testing showed the installed service serving
HTTPS on a host hostdoc had advertised as http:// — an HTTP request got 400.

Thread `scheme` into caddySnippet and prefix the site address with it
(http://host or https://host). An explicit http:// disables Caddy's
Automatic HTTPS so the served protocol matches the http:// link; --scheme
https opts into TLS and yields an https:// link + auto-HTTPS. Verified end
to end on Windows: http GET now 200 (was 400), no TLS/HTTP-3 listener,
/_meta still 403.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jkas2016
jkas2016 marked this pull request as ready for review July 4, 2026 19:49
@jkas2016
jkas2016 merged commit 1aebec6 into main Jul 4, 2026
3 checks passed
@jkas2016
jkas2016 deleted the feat/41-self-hosted-mode branch July 4, 2026 19:49
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: low Low priority released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] self-hosted 모드: 내 NAS/PC + 내 공인 IP로 발행 (AWS 불필요)

1 participant