Wiki
REST API
uVersion 서버 HTTP 엔드포인트의 전체 레퍼런스: auth, repos, files, locks, comments, watchlist, modification requests, builds, admin.
uVersion 서버는 JWT로 인증되는 JSON HTTP API를 제공합니다. 이 페이지는 데스크톱 클라이언트,
에디터 플러그인, 그리고 uversion CLI가 사용하는 모든 엔드포인트를 문서화합니다.
이를 직접 호출하여 uVersion을 내부 도구(커스텀 대시보드, 감사 스크립트, webhook 등)에
통합할 수 있습니다.
규약
기본 URL
문서화된 모든 경로는 인스턴스의 URL에 대한 상대 경로입니다. 예제는
https://uversion.mygamestudio.com을 사용합니다. 본인의 URL로 바꾸세요.
필수 헤더
| 헤더 | 값 |
|---|---|
Authorization | /api/auth/* 및 /api/server-info(그리고 /health)를 제외한 모든 /api/* 라우트에서 Bearer <jwt> |
Content-Type | JSON 본문이 있는 POST/PUT에는 application/json. 바이너리 업로드(build files)에는 application/octet-stream. |
Accept | application/json 권장(서버는 기본적으로 JSON을 반환합니다) |
일관된 응답 형식
모든 라우트는 다음 엔벨로프로 응답합니다:
{
"success": true,
"data": { /* payload */ }
}
오류 시:
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
success 필드는 항상 존재하며 요청이 성공했는지 여부를 나타냅니다.
성공 시 data 필드가 존재합니다. 실패 시 error 필드가 존재합니다.
둘이 함께 존재하는 경우는 없습니다.
페이지네이션
잠재적으로 긴 목록을 반환하는 라우트는 쿼리 파라미터로 limit과 offset을
받습니다(?limit=20&offset=40). 응답에는 반복을 쉽게 하기 위한 total과 has_more가
포함됩니다. 최대 limit: 100.
레이트 리미팅
전역 레이트 리미팅은 제거되었습니다(CLAUDE.md의 Security 섹션 참조). /api/auth/login만
레이트 리미팅됩니다: username당 15분마다 5회의 실패 시도. 유효한 시도는
카운트되지 않습니다.
토큰 버저닝
각 JWT에는 DB의 users.token_version에 해당하는 tv 필드(token version)가 포함됩니다.
auth 미들웨어는 모든 요청에서 이 값을 확인합니다. 사용자가 POST /api/auth/logout을 실행할 때,
관리자가 그 비밀번호를 재설정할 때, 또는 관리자가 그 계정을 비활성화할 때 token_version이 증가하며,
해당 사용자의 모든 머신에 있는 기존의 모든 토큰이 즉시 무효화됩니다.
Curl
터미널에서 API를 호출하려면:
TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}' | jq -r '.data.token')
curl -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories
인증
POST /api/auth/login
사용자를 인증하고 30일 유효한 JWT를 반환합니다.
본문:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-06-13T14:30:00Z",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead",
"is_active": true
}
}
}
오류:
401: 잘못된 자격 증명 또는 비활성 사용자429: 이 username에 대해 15분 창 내에 5회의 실패 시도에 도달
서버는 타이밍 공격을 무력화하기 위해 항상 argon2id::verify_password(사용자가 존재하지 않는 경우 더미 해시에 대해)를 실행합니다.
사용자의 존재 여부와 관계없이 응답에 걸리는 시간은 동일합니다.
POST /api/auth/refresh
비밀번호를 다시 입력하지 않고 JWT를 갱신합니다.
헤더: Authorization: Bearer <current_token>. 본문 없음.
Response 200: /login과 동일하며 만료가 30일 뒤로 연장되고 새 토큰이 반환됩니다.
오류:
401: 현재 토큰이 유효하지 않음, 만료됨, 또는token_version불일치
POST /api/auth/logout
users.token_version을 증가시켜 현재 사용자의 모든 토큰(모든 머신)을 무효화합니다.
사용자는 모든 곳에서 다시 로그인해야 합니다.
Response 200: { "success": true }.
POST /api/auth/validate
토큰이 아직 유효한지 확인합니다. 만료까지 7일 미만인 토큰의 경우, 서버는 응답에
refreshed_token을 포함합니다(자동 연장). 클라이언트는 이전 것 대신 이를 유지해야 합니다.
Response 200:
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
사용자 계정을 생성합니다(인증이 필요 없는 공개 라우트).
POST /api/auth/change-password
현재 사용자의 비밀번호를 변경합니다. token_version을 증가시켜 모든 이전 토큰을 무효화합니다.
리포지토리
GET /api/repositories
현재 사용자가 접근할 수 있는 리포지토리를 나열합니다(permissions 테이블로 필터링). 리포지토리에 대한 permission 패턴이 없는 사용자에게는 표시되지 않습니다.
쿼리 파라미터: limit, offset.
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"created_at": "2026-01-15T10:00:00Z"
},
{
"id": 2,
"name": "shared-assets",
"owner": "acme",
"description": "Shared asset library",
"current_revision": "3a4b5c6d...",
"file_count": 412,
"size_bytes": 980000000,
"created_at": "2026-02-01T09:00:00Z"
}
]
}
GET /api/repositories/{repo_id}
집계된 통계를 포함한 리포지토리의 세부 정보.
Response 200:
{
"success": true,
"data": {
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"dedup_ratio": 4.2,
"created_at": "2026-01-15T10:00:00Z",
"last_commit_at": "2026-05-15T08:30:00Z"
}
}
오류:
403: 이 리포지토리에 대한 접근 권한 없음404: 리포지토리가 존재하지 않음
POST /api/repositories
새 리포지토리를 생성합니다. create_repos capability가 필요합니다(기본적으로 admin).
본문:
{
"name": "new-project",
"description": "Optional description"
}
검증:
- name: 1~64자, 영숫자 + 하이픈 + 언더스코어. owner당 고유.
- description: 0~1024자. 선택 사항.
오류:
400: 잘못되었거나 너무 긴 이름403:create_reposcapability 누락409: 이 owner에 이 이름의 리포지토리가 이미 존재함
파일
POST /api/files/{repo_id}/upload-chunks
바이너리 청크의 배치를 업로드합니다. 동일한 청크(SHA-256 기준)는 자동으로 중복 제거됩니다.
서버는 이미 존재하는 청크를 다시 저장하지 않습니다. 응답은 각 청크에 대해 해시 + 크기 + 압축 크기를 반환하며,
다음 POST /commit에서 사용됩니다.
본문:
{
"chunks": [
{ "data": "<base64-encoded chunk bytes>" },
{ "data": "<base64-encoded chunk bytes>" }
]
}
Response 200:
{
"success": true,
"data": {
"chunks": [
{ "hash": "abc123...", "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "size": 2097152, "compressed_size": 891204 }
]
}
}
제한: 본문 최대 1 GB(security.max_body_size_files로 설정 가능). 더 큰 파일의 경우 여러 배치로 분할하세요.
POST /api/files/{repo_id}/commit
파일 목록과 그 청크로 원자적 커밋을 생성합니다. 모든 파일이 통과하거나 아무것도 통과하지 않습니다.
본문:
{
"message": "Updated main level + hero pose pass",
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"action": "modified",
"chunks": ["abc123...", "def456..."]
},
{
"path": "Content/Characters/NewVillain.uasset",
"action": "added",
"chunks": ["fed789..."]
},
{
"path": "Content/OldAsset.uasset",
"action": "deleted"
}
]
}
가능한 액션: added, modified, deleted.
added와 modified의 경우, chunks 배열에는 /upload-chunks가 반환한 해시가 들어갑니다.
deleted의 경우 chunks를 생략합니다.
Response 200:
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_changed": 3,
"bytes_uploaded": 88080384,
"bytes_deduped": 4194304,
"revision": 47
}
}
오류:
400: 빈 메시지, 액션이 없는 파일, 존재하지 않는 청크 참조403:checkincapability 없음, 또는 파일 중 하나에 대한 write permission 없음409: 수정된 파일에 대해 다른 사용자가 이미 잠금을 보유, 또는 동시 커밋(동일 파일에 대한 경쟁 상태)
GET /api/files/{repo_id}/snapshot
현재 리비전에서 리포지토리의 전체 상태를 반환합니다: 모든 파일, 그 리비전, 그리고 그 청크. clone 및 강제 sync 작업에서 클라이언트가 사용합니다.
쿼리 파라미터:
revision(선택): 과거 리비전의 스냅샷. 기본값: HEAD.
Response 200:
{
"success": true,
"data": {
"revision": 47,
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"revision": 12,
"size_bytes": 84934656,
"chunks": ["abc123...", "def456..."]
}
]
}
}
GET /api/files/{repo_id}/content
지정한 리비전에서 파일의 내용을 다운로드합니다(서버가 청크를 재조립합니다). clone 및 sync에서 사용합니다.
쿼리 파라미터:
path: 파일 경로(리포지토리 루트 기준 상대)revision(선택): 리비전 번호. 기본값: 최신.
Response 200: 파일의 바이너리 내용.
GET /api/files/{repo_id}/history
리포지토리 커밋의 페이지네이션된 이력.
쿼리 파라미터:
limit(1~100, 기본값 20)offset(기본값 0)path(선택): 파일 경로로 필터(이 파일을 수정한 커밋)author(선택): username으로 필터since(선택): ISO 8601 timestamp
Response 200:
{
"success": true,
"data": {
"commits": [
{
"hash": "7f3a9b1c...",
"author": "alice",
"author_id": 12,
"date": "2026-05-15T08:30:00Z",
"message": "Fixed lighting in main level",
"files_changed": 1,
"bytes_uploaded": 84934656
}
],
"total": 423,
"limit": 20,
"offset": 0,
"has_more": true
}
}
증분 sync
데스크톱 클라이언트가 효율적으로 동기화하기 위해 사용하는 엔드포인트:
GET /api/files/{repo_id}/sync: 어떤 리비전 이후 변경된 파일, delta sync용.GET /api/files/{repo_id}/deletions: 서버 측에서 삭제된 파일, 삭제를 로컬에 전파하기 위함.GET /api/files/{repo_id}/list: 리포지토리 파일 목록.POST /api/files/{repo_id}/checkout: 잠금을 획득하고 편집을 준비합니다.
잠금
POST /api/locks/{repo_id}/acquire
경로 목록에 대해 잠금을 획득합니다. 획득은 독립적입니다: acquired 목록에는 성공,
failed 목록에는 실패와 그 이유가 들어갑니다.
본문:
{
"paths": [
"Content/Maps/MainLevel.umap",
"Content/Characters/Hero.uasset"
]
}
Response 200:
{
"success": true,
"data": {
"acquired": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"file_id": 12345,
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T14:30:00Z",
"expires_at": "2026-05-15T15:30:00Z"
}
],
"failed": [
{
"path": "Content/Characters/Hero.uasset",
"reason": "ALREADY_LOCKED",
"lock_holder": "bob",
"lock_acquired_at": "2026-05-15T13:00:00Z"
}
]
}
}
잠금은 heartbeat 없이 60분 후에 만료됩니다. expires_at 필드는 이 기한을 반영합니다.
잠금을 갱신하려면 주기적으로 /heartbeat를, 해제하려면 /release를 호출하세요.
failed 이유:
ALREADY_LOCKED: 다른 사용자가 잠금을 보유(lock_holder와lock_acquired_at필드가 채워짐)PERMISSION_DENIED: 이 경로에 대한 write permission 없음INVALID_PATH: 잘못된 경로(절대 경로,..포함 등)
POST /api/locks/{repo_id}/release
본인이 소유한 잠금을 해제합니다. 본문:
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true에는 force_unlock capability가 필요합니다. 이 작업은 감사됩니다.
POST /api/locks/{repo_id}/heartbeat
본인의 잠금에서 last_heartbeat_at을 업데이트합니다. 관리자가 활동을 볼 수 있기를 원하는
장시간 실행되는 CI 작업의 경우 5분마다 실행하는 것을 권장합니다.
GET /api/locks/{repo_id}/status
리포지토리의 모든 활성 잠금을 나열하며, 이름을 직접 반환하기 위해 users와 files에 join합니다.
쿼리 파라미터: user(username으로 필터), limit, offset.
Response 200:
{
"success": true,
"data": {
"locks": [
{
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T16:00:00Z"
}
],
"total": 1
}
}
댓글 및 리뷰
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
커밋의 댓글을 스레드 형태로(부모 → 자식) 나열합니다.
Response 200:
{
"success": true,
"data": [
{
"id": 42,
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"author_id": 12,
"author_username": "alice",
"body": "LGTM, ship it",
"parent_id": null,
"created_at": "2026-05-15T15:00:00Z",
"replies": [
{
"id": 43,
"author_username": "bob",
"body": "Thanks!",
"parent_id": 42,
"created_at": "2026-05-15T15:05:00Z"
}
]
}
]
}
POST /api/comments/{repo_id}/comments
댓글을 생성합니다. file_path는 선택 사항입니다(커밋 댓글 vs. 파일 댓글). 기존 스레드에 답글을 달려면 parent_id를 사용합니다.
본문:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
커밋에 리뷰를 제출합니다. approve_changes capability가 필요합니다.
본문:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
허용되는 status: approved, changes_requested, pending.
관심 목록
GET /api/watchlist/{repo_id}/watchlist
이 리포지토리에서 현재 사용자의 watch 패턴을 나열합니다.
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"user_id": 12,
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"],
"created_at": "2026-04-10T10:00:00Z"
}
]
}
POST /api/watchlist/{repo_id}/watchlist
watch 패턴을 추가합니다. 본문:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
가능한 이벤트: commit(커밋이 매칭되는 파일을 수정), lock(잠금이 획득됨),
review(리뷰가 게시됨).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
watch 패턴을 제거합니다. { "success": true }를 반환합니다.
프로덕션 보드(작업)
이전 modification-requests API는 제거되었습니다: 프로덕션 보드가 이를 흡수합니다
(요청이 카드가 됩니다, origin='request'). 엔드포인트는 /api/tasks/{repo_id} 아래에
중첩되어 있습니다.
| 라우트 | 설명 |
|---|---|
GET /api/tasks/{repo_id}/board | 전체 보드: 컬럼 + 카드 |
POST /api/tasks/{repo_id}/tasks | 카드 생성 |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | 카드 업데이트 / 삭제 |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | 카드에 사용자 할당 |
GET /api/tasks/{repo_id}/assignable-users | 할당 가능한 사용자 목록 |
POST /api/tasks/{repo_id}/columns | 보드 컬럼 구성 |
사용 가능한 하위 라우트: 카드 댓글, assets 및 커밋으로의 링크, 그리고 첨부 파일
(커버 이미지 포함). 컬럼 구성에는 manage_board capability(admin / lead)가 필요합니다.
빌드
POST /api/builds/{repo_id}/upload?hash=<sha256>
빌드 파일을 <storage_path>/builds/<hash>로 업로드합니다.
서버는 쿼리 파라미터로 제공된 SHA-256을 수신된 내용과 대조하여 검증합니다. 해시가 서버 측에 이미 존재하는 경우,
다시 복사하지 않고 즉시 200을 반환합니다(build storage 측 중복 제거).
헤더: Content-Type: application/octet-stream.
본문: 원시 바이너리(JSON 래핑 없음).
제한: 파일당 8 GB.
오류:
400: hash 쿼리 파라미터 누락 또는 잘못됨, 또는 계산된 SHA-256이 제공된 해시와 다름413: 파일이 8 GB 초과
POST /api/builds/{repo_id}/publish
모든 파일을 업로드한 후 빌드의 manifest를 등록합니다. publish_builds capability가 필요합니다.
본문:
{
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"executable_path": "HeroRPG/Binaries/Win64/HeroRPG.exe",
"release_notes": "Nightly build of main branch, 2026-05-15",
"files": [
{ "path": "HeroRPG.exe", "hash": "abc123...", "size_bytes": 12345678 },
{ "path": "HeroRPG/Content/Paks/pak0.pak", "hash": "def456...", "size_bytes": 2147483648 }
]
}
GET /api/builds/{repo_id}
게시된 빌드를 나열합니다. 다운로드 링크를 보려면 download_builds capability가 필요합니다.
Response 200:
{
"success": true,
"data": [
{
"id": 5,
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"published_by": "ci-nightly",
"published_at": "2026-05-15T03:00:00Z",
"size_bytes": 2159829326,
"file_count": 142,
"release_notes": "Nightly build of main branch, 2026-05-15"
}
]
}
GET /api/builds/{repo_id}/{build_id}/file
빌드에서 파일을 다운로드합니다. download_builds capability가 필요합니다. 빌드의 manifest는 GET /api/builds/{repo_id}/{build_id}/manifest로 이용할 수 있습니다.
관리자
모든 /api/admin/* 라우트에는 사용자가 최소한 하나의 admin capability
(엔드포인트에 따라: manage_users, manage_permissions, manage_rules 등)를 가지고 있어야 합니다.
아래는 간략한 문서입니다. 대부분은 표준 REST 패턴(GET/POST/PUT/DELETE)을 따릅니다.
| 라우트 | Capability | 설명 |
|---|---|---|
GET/POST /api/admin/users | manage_users | 사용자 나열 / 생성 |
PUT/DELETE /api/admin/users/{id} | manage_users | 업데이트 / 삭제 |
POST /api/admin/users/{id}/reset-password | manage_users | 비밀번호 재설정, token_version 증가 |
GET/POST /api/admin/groups | manage_users | 그룹 관리 |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | 경로별 glob permission 규칙 |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | checkin 전 검증 규칙 |
GET/POST /api/admin/repositories/{repo_id}/webhooks | manage_rules | Discord / Slack / Teams / custom |
GET /api/admin/audit | view_all_activity | 필터링 및 페이지네이션된 audit log |
GET /api/admin/stats | view_all_activity | Storage metrics, dedup ratio, 성장 |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | 실행 없이 GC 미리보기 |
POST /api/admin/repositories/{repo_id}/gc | admin | garbage collection 실행 |
POST /api/admin/locks/{id}/force-release | force_unlock | 제3자 잠금 강제 해제, 감사됨 |
상태 확인
GET /health
서버가 데이터베이스에 접근할 수 있으면 200 OK를 반환하는 인증되지 않은 엔드포인트.
로드 밸런서 또는 모니터링 상태 확인(Prometheus blackbox, Datadog synthetic 등)에 사용됩니다.
Response 200: OK(text/plain).
Response 503: 데이터베이스에 도달할 수 없는 경우.
GET /api/server-info
서버 메타데이터(버전 등)를 반환하는 공개된 인증되지 않은 엔드포인트.
/health와 마찬가지로 auth 미들웨어로 보호되지 않습니다.
오류 형식
오류 시, 응답은 적절한 HTTP status를 동반한 { "success": false, "error": "..." }입니다.
error 필드는 사람이 읽을 수 있는 메시지입니다. 안정적인 기계 판독 코드가 필요한 클라이언트는
접두사를 파싱하세요(예: "Permission denied: ..."는 항상 "Permission denied"로 시작합니다).
| HTTP | 의미 | 언제 |
|---|---|---|
| 400 | Bad request | 잘못된 파라미터, 잘못된 JSON, 본문이 너무 큼(하드 413 제한 이전) |
| 401 | Not authenticated | 토큰 없음, 만료됨, 잘못된 서명, 또는 token_version 불일치 |
| 403 | Permission denied | capability 누락, 또는 경로 / 리포지토리에 대한 permission 없음 |
| 404 | Resource not found | 리포지토리 / 파일 / 커밋 / 사용자가 존재하지 않거나 접근 불가 |
| 409 | Conflict | 잠금이 이미 보유됨, 동시 커밋, 고유 제약 위반 |
| 413 | Payload too large | 본문이 제한을 초과(/files에서 1 GB, /builds/upload에서 8 GB) |
| 429 | Too many requests | /auth/login에만 해당(사용자별 레이트 리미팅) |
| 500 | Internal server error | DB 오류, IO 오류. 서버 측에 로그됨. 버그를 보고할 경우 timestamp를 첨부하세요. |
| 503 | Service unavailable | 데이터베이스에 도달할 수 없음(health check) 또는 마이그레이션 진행 중 |