# API 레퍼런스

기계가 읽는 명세는 `GET /openapi.json`(라우트 표 `server/routes.js`에서 자동 생성 — 이 문서와 어긋나면 명세가 맞다). 아래 예시는 실제 서버 응답이다.

- 읽기 전용: **GET·HEAD만** 받는다. 다른 메서드는 405. 올리기·수정 API는 일부러 없다(→ `decisions.md` D3). 릴리스는 서버 PC에서 `scripts/us.mjs`로 올린다.
- 모든 JSON은 UTF-8, 오류는 항상 `{"error":{"code","message", …}}`.
- 시간은 RFC 3339(UTC, `…Z`).

## 인증

`/v1/*`는 키가 필요하다. 키는 헤더로만 받는다(주소·쿼리에 넣으면 로그·프록시에 남으므로 **받지 않는다**).

```
X-Update-Key: upk_k91ea0eda_2PIjuSRK-IoZ2Xgey7lPrX8CxN8YIBQ_
Authorization: Bearer upk_k91ea0eda_…      # 위와 같은 뜻 (헤더를 못 넣는 도구용)
```

| 상황 | 응답 |
|---|---|
| 키 없음·틀림·폐기 | `401 unauthorized` + `WWW-Authenticate: Bearer realm="update"` |
| 키는 맞지만 그 제품을 볼 수 없음 | `404 no_product` — 없는 제품과 **똑같이** 답한다(제품 존재 여부를 흘리지 않음) |
| 틀린 키를 같은 IP가 60초에 20번 넘게 보냄 | `429 too_many_attempts` + `Retry-After` (그 IP는 맞는 키도 잠시 막힘). 키를 아예 안 보낸 요청은 세지 않는다 |

공개(키 불필요): `/`, `/healthz`, `/llms.txt`, `/openapi.json`, `/docs/<이름>.md`.

## 플랫폼 이름

한 플랫폼 = `os-arch` (예: `windows-x86_64`).

| os | 별명 | | arch | 별명 |
|---|---|---|---|---|
| `windows` | win, win32, win64 | | `x86_64` | amd64, x64, x86-64 |
| `linux` | ubuntu, debian | | `aarch64` | arm64 |
| `darwin` | macos, mac, osx | | `i686` | i386, x86, ia32 |
| `any` | all, `*`, universal | | `armv7` | armv7l, arm, armhf |
| | | | `any` | all, `*`, universal |

별명은 서버가 한 이름으로 합친다. **Node의 `process.platform/arch`, Python의 `platform.system()/machine()`, `uname -s/-m` 값을 그대로 보내도 된다.**
`any`는 OS·CPU에 상관없는 파일(스크립트·jar·웹 번들)용. 클라이언트에 맞는 파일은 이 차례로 찾는다: `os-arch` → `os-any` → `any-arch` → `any-any`.

### 아키텍처(CPU) 단위 지원 — x64/arm64 × Windows/Ubuntu/macOS

- **x64 클라이언트는 arm64 파일을 절대 받지 않고, arm64는 x64 파일을 받지 않는다.** 각자 자기 `os-arch` 칸만 본다(예: Ubuntu arm64 = `linux-aarch64`, Windows x64 = `windows-x86_64`).
- **플랫폼마다 최신이 따로 센다.** 최신 릴리스에 그 플랫폼 빌드가 없으면(예: arm 빌드가 아직 안 끝남) **그 빌드가 있는 가장 최신 릴리스**를 준다. 그래서 `linux-aarch64`만 2.0.0이고 나머지가 2.1.0이어도 arm 사용자는 "이미 최신(204)"이거나 2.0.0으로 정상 안내된다. 제품 응답(`/v1/products/{p}`)의 `latest.<채널>.by_platform`과 웹 화면이 플랫폼별 최신 버전을 보여 준다(뒤처진 칸은 점선·버전 표시).
- **빌드가 아직 없는 아키텍처**는 `404 no_release` + `error.available_platforms`(그 채널에 올라온 플랫폼 목록)로 진단할 수 있다.
- **에뮬레이션 허용(`emulated=1`, 기본 꺼짐):** 이 CPU용 빌드가 없을 때만, 에뮬레이션·호환 계층으로 돌 수 있는 다른 CPU 빌드를 쓴다 — Windows arm64→`windows-x86_64`→`windows-i686`, Windows x64→`windows-i686`, macOS arm64→`darwin-x86_64`(Rosetta). 네이티브와 OS 공용(`any`) 파일이 항상 먼저이고, 응답에 `emulated:true`가 붙는다. Linux는 qemu·multiarch 설정에 달려 있어 자동으로 가정하지 않고, 거꾸로(x64 PC에 arm64 빌드)는 안 된다.
- **같은 플랫폼의 여러 형식**(Ubuntu의 `deb`·`tar.gz`·`appimage`, 배포판 전용 `deb-ubuntu2404` 같은 사용자 정한 이름)은 `kind`로 고른다.
- 빌드가 아키텍처마다 따로 끝나면 `us.mjs add-files`로 **이미 올린 릴리스에 나중에 덧붙인다**(`operations.md`). 덧붙이기만 되고 있는 칸은 못 바꾼다.

## GET /v1/products/{product}/check — 업데이트 확인 (대부분 이것만)

| 쿼리 | | 설명 |
|---|---|---|
| `current` | 필수 | 지금 설치된 버전(SemVer) |
| `os`, `arch` | 필수 | 또는 `target=os-arch` 한 개로 |
| `channel` | | 기본 `stable` |
| `kind` | | 같은 플랫폼에 형식이 여럿일 때(`msi`·`exe`·`deb`·`zip`·`tar.gz`·`appimage` …) 원하는 것 |
| `emulated` | | `1`이면 에뮬레이션으로 돌릴 수 있는 다른 CPU 빌드도 허용(위 '아키텍처 단위 지원') |

```
GET /v1/products/mytool/check?current=1.3.0&os=windows&arch=AMD64
200
{
  "update_available": true,
  "current": "1.3.0",
  "mandatory": false,
  "resolved_target": "windows-x86_64",
  "emulated": false,
  "latest": { "version": "1.4.2", "channel": "stable", "pub_date": "2026-10-15T16:38:14.898Z",
              "notes": "로그인 오류 수정", "min_supported_version": "1.3.0", "meta": { "restart": true } },
  "download": {
    "kind": "msi", "filename": "mytool-1.4.2-x64.msi", "size": 15,
    "sha256": "8eba069ae1112f06fc005bc0bc1792f3ec8109538eb40278b47fb480e9086af4",
    "url": "https://update.ngnear.dev/v1/products/mytool/releases/1.4.2/files/mytool-1.4.2-x64.msi",
    "signature": "dW50cnVzdGVkIGNvbW1lbnQ=",
    "artifacts": [ { "...같은 칸의 모든 형식: kind, filename, size, sha256, url, signature?" } ]
  }
}
```

- **204** (본문 없음): 이미 최신이거나 서버보다 앞선 버전. 업데이트 없음이 정상 결과다.
- `mandatory`: 릴리스가 필수로 표시됐거나 `current < min_supported_version`일 때 `true`. true면 사용자가 건너뛰게 두지 않는다.
- `download`는 대표 파일 하나를 평평하게 펴 둔 것이고, 같은 플랫폼의 다른 형식은 `artifacts`에 모두 있다. `?kind=`를 주면 그 형식이 대표가 된다.
- `signature`: 게시자가 `--sig`로 붙인 서명 문자열을 서버가 **그대로 전달**한다(서버는 검증하지 않는다 → `integration.md` '서명').
- `meta`: 제품이 정한 자유 JSON(예: `{"restart":true}`). 서버는 해석하지 않는다.
- 오류: `400 bad_version`(current가 SemVer 아님)·`missing_target`·`bad_os`·`bad_arch`·`bad_target`·`bad_channel` · `404 no_release`(이 채널·플랫폼용 릴리스 없음, `error.available_platforms`에 올라온 플랫폼) · `404 no_product`.

## GET /v1/products/{product}/latest — 최신 매니페스트

Tauri 업데이터의 `latest.json`과 같은 모양(`version`·`notes`·`pub_date`·`platforms{os-arch:{url,signature}}`)에 칸을 더했다.
쿼리: `channel`, `target`(또는 `os`+`arch`), `kind`. **target을 주면 그 칸만**(대체로 찾은 칸 이름이 `resolved_target`), 안 주면 모든 플랫폼.

```
GET /v1/products/mytool/latest?target=linux-x86_64
200
{ "product":"mytool", "version":"1.4.2", "channel":"stable", "pub_date":"…", "notes":"로그인 오류 수정",
  "mandatory":false, "min_supported_version":"1.3.0", "meta":{"restart":true},
  "platforms": { "linux-x86_64": { "kind":"deb", "filename":"mytool_1.4.2_amd64.deb", "size":15, "sha256":"e60e8bcb…", "url":"https://…/files/mytool_1.4.2_amd64.deb", "artifacts":[…] } },
  "resolved_target": "linux-x86_64" }
```

현재 버전 비교는 하지 않는다(그건 `/check`). `404 no_release`: 그 채널(·플랫폼)에 릴리스가 없다.

## 목록·상세

| 주소 | 설명 |
|---|---|
| `GET /v1/me` | `{key_id,label,products}` — 키 시험용 |
| `GET /v1/products` | 이 키가 볼 수 있는 제품과 채널별 최신 `{id,name,description,homepage,latest:{<채널>:{version,channel,pub_date,mandatory,platforms[],by_platform:{<os-arch>:{version,pub_date}}}}}` |
| `GET /v1/products/{p}` | 제품 하나(같은 모양) |
| `GET /v1/products/{p}/releases?channel=&limit=` | 새 버전부터. 회수(yank)된 것은 빠진다. `limit` 기본 50·최대 200 |
| `GET /v1/products/{p}/releases/{version}` | 한 릴리스의 모든 플랫폼 파일과 주소. 회수됐으면 `410 yanked`(+`reason`) |

## GET /v1/products/{p}/releases/{version}/files/{name} — 내려받기

- 응답 헤더: `Content-Length`, `ETag: "<sha256>"`, `X-Checksum-SHA256`, `Accept-Ranges: bytes`, `Content-Disposition: attachment`, **`Cache-Control: private, no-cache`**.
- `If-None-Match: "<sha256>"` → 304. `Range: bytes=a-b | a- | -n` → 206(이어받기). 여러 구간은 받지 않고 전체를 준다. 범위 밖이면 416.
- `HEAD`도 된다(크기·해시만 미리 보기).
- 릴리스에 등록된 파일 이름만 내려준다. 회수된 릴리스는 `410 yanked`.
- 받은 뒤 **크기와 sha256을 확인**한 다음에 설치한다(`download.sha256`/`size` 또는 `X-Checksum-SHA256`).

## 웹·문서(공개)

| 주소 | |
|---|---|
| `GET /` | 사람용 목록(제품·최신 버전·날짜·플랫폼, 파일 주소 없음). 서버의 `PUBLIC_LISTING=0`이면 숨김, 제품을 `--unlisted`로 만들면 그 제품만 빠진다 |
| `GET /healthz` | `{"ok":true,"service":"update-server","version":"…"}` |
| `GET /llms.txt` | LLM·에이전트용 안내(주소가 이 서버 것으로 치환됨) |
| `GET /openapi.json` | OpenAPI 3.1 |
| `GET /docs/{architecture,api,integration,operations,decisions}.md` | 이 문서들 |

## 오류 코드 한눈에

`unauthorized`(401) · `too_many_attempts`(429) · `no_product`(404, 볼 수 없는 제품 포함) · `no_release`(404) · `no_file`(404) · `yanked`(410) · `bad_version`·`missing_target`·`bad_os`·`bad_arch`·`bad_target`·`bad_channel`·`bad_request`(400) · `method_not_allowed`(405) · `not_found`(404, 없는 주소) · `no_doc`(404). 5xx는 `internal`(자세한 건 서버 로그).

## 접근 기록

서버는 `data/logs/access.jsonl`에 한 줄씩 남긴다: `ts, ip, method, path, status, ms, key(id), label, client(X-Update-Client 헤더, 없으면 User-Agent), bytes`. **키 원문은 남지 않는다.** 클라이언트가 `X-Update-Client: mytool/1.4.2` 같은 헤더를 보내 주면 누가 어느 버전인지 알아보기 쉽다(권장, 필수 아님).
