uVersion
한국어
다운로드 →

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-TypeJSON 본문이 있는 POST/PUT에는 application/json. 바이너리 업로드(build files)에는 application/octet-stream.
Acceptapplication/json 권장(서버는 기본적으로 JSON을 반환합니다)

일관된 응답 형식

모든 라우트는 다음 엔벨로프로 응답합니다:

{
  "success": true,
  "data": { /* payload */ }
}

오류 시:

{
  "success": false,
  "error": "Permission denied: capability 'manage_users' required"
}

success 필드는 항상 존재하며 요청이 성공했는지 여부를 나타냅니다. 성공 시 data 필드가 존재합니다. 실패 시 error 필드가 존재합니다. 둘이 함께 존재하는 경우는 없습니다.

페이지네이션

잠재적으로 긴 목록을 반환하는 라우트는 쿼리 파라미터로 limitoffset을 받습니다(?limit=20&offset=40). 응답에는 반복을 쉽게 하기 위한 totalhas_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_repos capability 누락
  • 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. addedmodified의 경우, 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: checkin capability 없음, 또는 파일 중 하나에 대한 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_holderlock_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/usersmanage_users사용자 나열 / 생성
PUT/DELETE /api/admin/users/{id}manage_users업데이트 / 삭제
POST /api/admin/users/{id}/reset-passwordmanage_users비밀번호 재설정, token_version 증가
GET/POST /api/admin/groupsmanage_users그룹 관리
GET/POST /api/admin/permissions/{repo_id}manage_permissions경로별 glob permission 규칙
GET/POST /api/admin/repositories/{repo_id}/rulesmanage_rulescheckin 전 검증 규칙
GET/POST /api/admin/repositories/{repo_id}/webhooksmanage_rulesDiscord / Slack / Teams / custom
GET /api/admin/auditview_all_activity필터링 및 페이지네이션된 audit log
GET /api/admin/statsview_all_activityStorage metrics, dedup ratio, 성장
GET /api/admin/repositories/{repo_id}/gc/previewadmin실행 없이 GC 미리보기
POST /api/admin/repositories/{repo_id}/gcadmingarbage collection 실행
POST /api/admin/locks/{id}/force-releaseforce_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의미언제
400Bad request잘못된 파라미터, 잘못된 JSON, 본문이 너무 큼(하드 413 제한 이전)
401Not authenticated토큰 없음, 만료됨, 잘못된 서명, 또는 token_version 불일치
403Permission deniedcapability 누락, 또는 경로 / 리포지토리에 대한 permission 없음
404Resource not found리포지토리 / 파일 / 커밋 / 사용자가 존재하지 않거나 접근 불가
409Conflict잠금이 이미 보유됨, 동시 커밋, 고유 제약 위반
413Payload too large본문이 제한을 초과(/files에서 1 GB, /builds/upload에서 8 GB)
429Too many requests/auth/login에만 해당(사용자별 레이트 리미팅)
500Internal server errorDB 오류, IO 오류. 서버 측에 로그됨. 버그를 보고할 경우 timestamp를 첨부하세요.
503Service unavailable데이터베이스에 도달할 수 없음(health check) 또는 마이그레이션 진행 중