• LINKS 1.0.6 — CDN 압축으로 인한 Player artifact 검사 실패 차단

    LINKS 1.0.6

    이 patch 릴리스는 이전 릴리스 1.0.5(604d5ffec1)부터 최종 소스 735ac60b1c까지를 묶었습니다. 루트 패키지는 1.0.6이고 ARCA pin은 exact 0.1.18로 그대로입니다. Cloudflare 같은 CDN·reverse proxy가 Player runtime artifact 응답을 압축해 브라우저의 무결성 검사가 실패하던 문제를 막았습니다. 바뀐 application 코드는 Player의 응답 header 두 곳입니다. schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.5 → 1.0.6은 sh update.sh만 실행합니다(초기화 없음)

    1. 갱신 방법: 1.0.5에서는 배포 디렉터리에서 sh update.sh만 실행합니다. schema는 바뀌지 않았고(infra/schema·infra/migrations·.env.example이 1.0.5와 같고 migration은 0개) reset.sh를 실행하지 않습니다. 7개 배포 파일도 1.0.5와 같으며, 바뀌는 것은 image뿐입니다. 갱신 전 backup(sh backup.sh)은 선택입니다. 1.0.5부터 LINKS container는 host Watchtower의 대상이 아니므로 실행 시각을 맞출 필요가 없습니다. 1.0.4 이하에서 올 때는 1.0.5 노트의 첫 갱신 주의를 따릅니다.
    2. 고친 문제: CDN·reverse proxy가 Player runtime artifact 응답을 압축하면 Content-Encoding이 붙고 Content-Length가 빠지거나 strong ETag가 약해집니다. 그러면 게임 실행이 fetching-runtime 단계의 REQUEST_FAILED(진단 RuntimeCacheError ARTIFACT_METADATA)로 실패했습니다. 이제 Player가 artifact 응답(GET·HEAD·206)에 Cache-Control: private, max-age=0, must-revalidate, no-transform을, save GET·POST 응답에 Cache-Control: no-store, no-transform을 보냅니다. Cloudflare는 origin의 no-transform이 있으면 압축하지 않고 Content-Length를 유지합니다. 브라우저의 bytes·Content-Length·strong ETag 검사는 완화하지 않았습니다.
    3. CDN 요건과 확인 방법: 운영 가이드(docs/운영_가이드.md)의 'CDN·reverse proxy의 artifact 변형' 절을 따릅니다. /player/*에는 Cache-Control을 지우거나 덮어쓰는 proxy 규칙(Transform Rules, Cache Rules의 header 변경 등)과 본문을 바꾸는 기능(Rocket Loader, Email Obfuscation 등)을 적용하지 않습니다. 갱신 뒤 공개 주소에서 실제 browser와 같은 Accept-Encoding으로 artifact URL을 HEAD합니다. 응답에 십진수 Content-Length, "sha256-…" strong ETag, no-transform이 있고 Content-Encoding이 없어야 합니다. gateway(30007)에 직접 보낸 결과와 header가 다르면 proxy 설정을 고칩니다. token과 실제 경로는 log·신고·문서에 남기지 않습니다.
    4. NAS 조건 gate: 릴리스 전에 격리 docker:24.0.2-dind에서 이 갱신을 검증했습니다. host Compose는 공식 Docker Compose v2.20.1 binary(release의 게시 checksum으로 확인)였고, stager는 1026:100, 원본 library 최상위는 0:0 000에 1026만 여는 ACL이었습니다. 설치된 1.0.5 update.sh로 1.0.6 갱신, VX Ace 준비, gateway를 거친 실제 Player session의 artifact header 확인, 재-update까지 모두 기대대로 끝났습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)와 실제 Cloudflare 경유 결과는 확인 전입니다.

    NAS 절차(1.0.5에서):

    cd /volume1/docker/links
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.6 (catalog 484addc10c12), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    Player 응답 header

    • 상수 artifactCacheControl(artifact route의 GET·HEAD·206이 같은 header 경로를 씁니다)과 saveCacheControl(save POST 200/201, save GET binary)을 추가했습니다.
    • 기존 private·max-age=0·must-revalidate·no-store 의미는 그대로이고 no-transform만 더했습니다. 브라우저 cache 판단, CSP, origin·session, save 형식은 바뀌지 않았습니다.
    • gateway Caddy는 encode를 쓰지 않고 Player 응답 header를 그대로 전달하므로 바뀌지 않았습니다.
    • problem 응답, /player/*/session·/launch JSON, /player/frame과 catalog API 응답은 header 검사 대상이 아니어서 바꾸지 않았습니다.

    문서

    • 운영 가이드: 'CDN·reverse proxy의 artifact 변형'(금지 사항, 증상, Cloudflare 설정 주의, HEAD 확인 방법)
    • API 계약(Artifact·Save의 Cache-Control 값), 브라우저 게임 런타임 계약(runtime cache HEAD 검사와 no-transform의 관계), 완료 기준선

    알려진 제한(후속 과제)

    • 실제 Cloudflare를 거친 artifact header 확인과 VX Ace 실제 browser acceptance는 배포 뒤 확인할 항목이며 지금은 NOT RUN입니다.
    • Cloudflare Enterprise zone에서 Origin Cache Control이 꺼져 있으면 no-transform만으로 압축 차단을 보장하지 못합니다. 운영 가이드에 이 조건을 더하는 문서 보완이 후속입니다. 위 업그레이드 주의 3의 HEAD 확인이 이 경우도 검출합니다.
    • 1.0.5와 같은 제한:
      • 실제 NAS(Synology Compose build, Synology ACL·btrfs)에서의 갱신·VX/VX Ace package, PowerShell coordinator 전체 실행은 NOT RUN입니다.
      • update.sh는 network가 빠진 container를 스스로 감지하지 않습니다. 운영 가이드 '실패와 되돌리기'로 복구합니다.
      • 재확인→게시 간격이 128 MiB에서 약 1.2초 늘어납니다(봉인 사본 검증은 게임 크기에 비례).
      • /artifacts 쓰기 거부는 기존 분류를 따르고, load 뒤 권한 변경은 ARTIFACT_INVALID로 끝납니다.
      • 소유 계정이 섞인 sealed store는 지원하지 않습니다.
      • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다(ARCA 후속).
      • 3단계 이후 실패하면 coordinator가 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며, edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress는 network로 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다.

    검증과 남은 범위

    • PASS: no-transform 브랜치(병합 48e9655)
      • Player unit RED → GREEN: 수정 전 app에서 artifact·save Cache-Control 단언 2건 FAIL, 수정 뒤 apps/player/src 9개 파일·92개 test PASS(HEAD·Range 206 단언 포함)
      • 전체 unit 149개 파일·1,190개 test, static(Prettier·ESLint·typecheck·docs-static), 문서 검사, git diff --check
      • 운영 infra/docker/Caddyfile과 같은 pinned Caddy image로 합성 upstream을 거친 gateway 통과 확인(Content-Encoding 없음, Content-Length 유지)
      • 리뷰 최종 판정: 병합 가능(필수 수정 0, P3 1건은 위 Cloudflare Enterprise 문서 보완)
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건)
      • 환경: 격리 docker:24.0.2-dind(overlay2), host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했습니다. registry에는 실제 1.0.5 image와 이 릴리스의 1.0.6 image를 넣었습니다. 배포 파일은 1.0.5 tag와 이 릴리스 커밋의 Git blob입니다.
      • 운영과 같은 계정과 원본 권한: scanner·stager 1026:100, ./data bind store, 원본 library 최상위 0:0 000 + ACL u:1026:r-x.
      • 1.0.5 설치 → reset.sh → update.sh: exit 0. 12개 service, network 불일치 0, 모두 label false, readiness 0/0.
      • 1.0.6 게시(registry latest = 1.0.6, asset 없음) 뒤 설치된 1.0.5 update.sh: exit 0(53초), Updated to 1.0.6 .... 7개 파일이 release와 같고 .tmp는 0입니다. app container가 1.0.6 image로 다시 만들어졌고 12개 service 모두 label false, network 불일치 0, unhealthy 0, package·runtime readiness exit 0, coordinator 재시작 0, catalog·Player version 1.0.6이었습니다.
      • VX Ace 준비(1.0.6): seal(runtime-package-stager, 1026:100) → stage → Builder → verify-source → publish → build ready. package·build job 모두 attempt 1에 성공했고 모든 one-shot이 exit 0이며 label false였습니다. sealed 항목은 모두 1026:100이고, Builder 계정의 읽기·탐색 실패는 0입니다.
      • gateway 경유 실제 Player session: launch broker와 같은 DB 경로로 발급한 launch grant를 gateway(30007)의 POST /player/<key>/launch로 교환했습니다. 그 session으로 entrypoint(host-launch.json), player module, runtime JS, runtime WASM(44 MB), game package 5개 artifact를 Accept-Encoding: gzip, deflate, br, zstd로 요청했습니다. HEAD·GET 200과 Range 206 모두 Cache-Control … no-transform, 십진수 Content-Length, strong "sha256-…" ETag가 있었고 Content-Encoding은 없었습니다. GET 본문의 크기와 digest가 Content-Length·ETag와 일치했고, 206에는 Content-Range가 있었습니다. 브라우저 대신 curl을 썼고 Cloudflare는 거치지 않았습니다.
      • 설치 상태에서 재-update: exit 0(13초), 배포 파일 변화 없음, service container 재생성 없음(coordinator·migration one-shot만 재기동), 준비 상태 ready 유지, network 불일치 0, readiness 0/0.
    • PASS: 최종 1.0.6 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 다음을 다시 통과했습니다: 문서 검사(761개 파일 / Markdown 32개), static(Prettier·ESLint·typecheck·docs-static), git diff --check.
    • 게시 이미지: 같은 최종 소스에서 build했고, gate도 이 image로 실행했습니다. image 안에서 root version 1.0.6과 ARCA pin·설치본 0.1.18을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공과 header 확인을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • 실제 Cloudflare를 거친 artifact HEAD와 VX Ace 실제 browser 실행(배포 뒤 확인)
      • 브라우저 matrix(run-browser): 바뀐 것은 Cache-Control 값이고 browser·client 코드는 Cache-Control을 읽지 않습니다
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Synology ACL, Docker 24 btrfs)에서의 1.0.5 → 1.0.6 갱신
      • PowerShell coordinator 전체 실행
      • 실제 Gitea release·registry를 대상으로 한 전체 stack 갱신(gate는 합성 release 서버·registry)
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.6이고 최종 소스는 735ac60b1c입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.6 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:484addc10c1290a4ce89ca21f1b48d6160cc701dcb5c74fb756dfc2a73e2efdd
    • release asset은 없습니다. update.sh가 tag 1.0.6의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 7개 배포 파일은 모두 1.0.5와 같습니다.
    Downloads 2