조직
orgs 카테고리에서 API 작업 14개를 다룹니다.
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /v1/orgs | — |
| POST | /v1/orgs | org 생성 |
| PATCH | /v1/orgs/{orgSlug} | org의 표시 이름 변경 (runlot org rename) |
| DELETE | /v1/orgs/{orgSlug} | org 삭제 (runlot org delete) |
| GET | /v1/orgs/{orgSlug}/notifications | 최근 알림(docs/limits.md §3.4) |
| GET | /v1/orgs/{orgSlug}/members | Org 멤버(runlot org members) |
| POST | /v1/orgs/{orgSlug}/members | 기존 사용자를 멤버로 추가(운영 및 자동화용) |
| GET | /v1/orgs/{orgSlug}/invites | 대기 중인 초대(runlot member invites) |
| POST | /v1/orgs/{orgSlug}/invites | 이메일로 초대(runlot member invite) |
| DELETE | /v1/orgs/{orgSlug}/invites/{inviteId} | 초대 취소(runlot member uninvite) |
| GET | /v1/invites/{token} | 초대 미리보기(인증 없음) |
| POST | /v1/invites/{token}/accept | 초대 수락 |
| PUT | /v1/orgs/{orgSlug}/members/{userId} | 역할 변경(runlot org set-role) |
| DELETE | /v1/orgs/{orgSlug}/members/{userId} | 멤버 제거 또는 자진 탈퇴(runlot member rm, runlot member leave) |
GET /v1/orgs
operationId listOrgs
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 소유 조직 | object |
| 401 | — | — |
POST /v1/orgs
생성자는 admin이 됩니다. 개인 계정도 1인 조직으로 모델링되므로(docs/mvp-scope.md), 여기에 별도 경로는 없습니다.
operationId createOrg
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 201 | 생성된 조직 | Org |
| 401 | — | — |
| 409 | — | — |
PATCH /v1/orgs/{orgSlug}
admin 이상의 권한이 필요합니다. org.rename으로 감사 기록됩니다.
slug는 이 표면에 포함되지 않습니다. slug는 주소가 되는 이름입니다(<project>.<slug>.runlot.app, 인증서 SAN, 클론 주소, 고객 저장소에 커밋되는 runlot.json의 "org" 필드). 따라서 이를 변경하려면 이전 이름을 얼마간 유지해야 합니다 — 그리고 그 별칭이 바로 0027이 도입하고 0037이 제거한 것입니다(docs/org-settings.md §2).
operationId updateOrg
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 업데이트된 org | OrgMembership |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
DELETE /v1/orgs/{orgSlug}
admin 이상의 권한이 필요합니다. 이 작업은 되돌릴 수 없습니다.
confirm 필드에는 정확한 org slug를 입력해야 합니다(앞뒤 공백은 제거됩니다). 이 API는 이 검사를 강제하는데, 대시보드·CLI·스크립트라는 세 호출자가 이 표면을 사용하기 때문입니다 — 각자 따로 구현하면 누군가 빠뜨려도 아무도 알아채지 못합니다.
삭제는 org가 비어 있을 때만 성공합니다. 프로젝트, 도메인, 저장소, 또는 DNS 존이 남아 있으면 응답은 409 org_not_empty이며, 봉투(envelope)의 details.holdings에 종류별 개수와 몇 개의 이름이 나열됩니다. 그 이유는 스키마 수준입니다(0044, 0020, 0032의 RESTRICT 제약 조건): 행을 삭제해도 노드의 데이터는 그대로 남으며, 아무것도 가리키지 않는 데이터는 삭제할 수도 청구할 수도 없습니다.
그 외 두 가지 거부 사유가 있습니다: 개인 org는 personal_org를 반환하며(첫 로그인 시 생성된 org로, 다시 생성되지 않습니다), 유료 구독이 활성 상태이면 active_subscription을 반환합니다(카드에 요금이 계속 청구되는 동안 org가 사라지는 것을 막기 위함입니다).
org.delete 감사 이벤트는 org보다 오래 남습니다 — audit_events.org_id는 ON DELETE SET NULL을 사용하며, slug는 target에 남습니다.
operationId deleteOrg
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 204 | 삭제됨 | — |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
| 409 | 비어 있지 않음 (org_not_empty) · 개인 org (personal_org) · 활성 구독 (active_subscription) | Error |
GET /v1/orgs/{orgSlug}/notifications
한도, 백업, 복원 이벤트 목록입니다(notifications). 최신순입니다.
sentAt이 설정되어 있으면 이메일도 발송된 것입니다. 설정되지 않은 행은 아직 대기 중이거나 받을 이메일이 없는 경우입니다 — 후자의 경우 이 목록이 유일한 채널입니다: GitHub 로그인이 비공개 이메일을 사용하면 users.email은 NULL입니다(§3.3).
뷰어 이상.
operationId listNotifications
| 매개변수 | 위치 | 필수 | 타입 | 설명 |
|---|---|---|---|---|
limit | query | 아니오 | 정수 | — |
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 알림 목록 조회 | object |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/members
member 이상의 권한이 필요합니다.
operationId listMembers
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 멤버 목록 | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/members
이것은 초대가 아닙니다. 사용자는 이미 한 번 로그인해서 users에 존재해야 합니다(provider+subject로 식별). 그렇지 않으면 404 user_not_found를 반환합니다. 이미 멤버라면 409 already_member를 반환합니다 — 역할 변경은 PUT입니다.
사람을 초대하는 창구는 이 엔드포인트가 아니라 POST …/invites입니다(docs/members.md): GitHub의 subject는 숫자 id로, 관리자가 알 수 없는 값이므로 이 엔드포인트는 이미 그 값을 아는 자동화를 위한 것입니다.
admin 이상 권한이 필요합니다. member.add로 감사 기록됩니다.
operationId addMember
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 201 | 멤버 추가됨 | Member |
| 400 | — | — |
| 403 | — | — |
| 404 | 조직이 존재하지 않거나 사용자가 존재하지 않습니다(user_not_found) | Error |
| 409 | 이미 멤버입니다(already_member) | Error |
GET /v1/orgs/{orgSlug}/invites
Admin 이상. Member 이상은 멤버 목록을 볼 수 있지만, 초대 목록은 다릅니다 — 이는 아직 조직에 속하지 않은 사람들의 이메일이며, 명단이 아니라 관리자 작업의 상태입니다.
수락되었거나 취소된 초대는 나타나지 않습니다(이력은 감사 로그에 남습니다). 만료된 초대는 나타납니다 — 재전송은 거기서 관리자의 몫입니다.
operationId listInvites
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 초대 목록 | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/invites
Admin 이상. 이메일은 즉시 발송되지 않습니다 — cp-core 발송 주기가 큐를 훑습니다(sentAt이 비어 있으면 아직 대기 중이라는 뜻입니다).
같은 주소로 다시 호출하면 재전송됩니다: 새 초대 대신 같은 행이 업데이트되고(새 토큰과 만료 시각이 설정되며 역할도 바뀔 수 있습니다), 이때는 200을 반환합니다. 새 초대는 201을 반환합니다.
이미 멤버인 주소는 409 already_member를 반환합니다. 대기 중인 초대가 너무 많으면 409 too_many_invites를 반환합니다 — 이는 요금제 제한이 아니라 발송에 대한 남용 방지입니다.
초대는 7일간 유효하며 1회용이고 언제든 취소할 수 있습니다. member.invite로 감사 기록됩니다.
operationId createInvite
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 기존 초대 재전송 | Invite |
| 201 | 새 초대 | Invite |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
| 409 | 이미 멤버임(already_member) 또는 대기 중인 초대가 너무 많음(too_many_invites) | Error |
DELETE /v1/orgs/{orgSlug}/invites/{inviteId}
Admin 이상. 아직 발송되지 않은 이메일은 발송되지 않습니다 — 취소하면 발송 큐도 함께 비워집니다. 이미 수락되었거나 취소된 초대는 404를 반환합니다. member.uninvite로 감사 기록됩니다.
operationId revokeInvite
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 204 | 취소됨 | — |
| 403 | — | — |
| 404 | — | — |
GET /v1/invites/{token}
인증 없음. 초대 링크를 받는 사람은 아직 로그인하지 않았고, 로그인하기 전에 그것이 무엇인지 알아야 합니다. 토큰은 32바이트 무작위 값이라 열거할 수 없으며, 이를 여는 대가는 "이메일을 받은 사람이 조직 이름을 보게 되는 것"에서 끝납니다.
상태는 오류가 아니라 값입니다(state). 만료된 초대에 404를 반환하면 UI가 "링크가 잘못되었다"와 "링크가 오래되었다"를 구분할 수 없게 됩니다.
알 수 없는 토큰은 404를 반환합니다 — 오래전에 취소된 토큰과 같은 응답입니다.
operationId getInvite
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 초대 | InviteView |
| 404 | — | — |
POST /v1/invites/{token}/accept
로그인이 필요합니다. 로그인한 계정의 이메일이 초대받은 주소와 일치해야 한다는 규칙은 없습니다 — GitHub 비공개 이메일로 로그인한 사람은 users.email이 아예 없으며, 그런 규칙을 추가하면 모든 org에서 접근이 막히게 됩니다. 대신 초대는 7일간 유효하고, 1회용이며, 언제든 취소할 수 있고, 감사 기록에는 초대받은 주소와 수락한 계정이 모두 남습니다.
이미 멤버인 사람이라면 역할은 변경되지 않습니다 (alreadyMember: true). 초대가 admin을 강등시키는 경로가 된다면, 그 강등은 마지막 admin 검사를 통과하지 못할 것입니다.
member.accept로 감사 기록됩니다.
operationId acceptInvite
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 수락됨 | object |
| 401 | — | — |
| 404 | — | — |
| 409 | 이미 수락됨(invite_used), 취소됨(invite_revoked), 또는 만료됨(invite_expired) | Error |
PUT /v1/orgs/{orgSlug}/members/{userId}
admin 이상 권한이 필요합니다. 마지막 admin은 강등할 수 없습니다 (409 last_admin) — 본인도 포함됩니다. 이 규칙이 없으면 조직을 관리할 수 있는 사람이 아무도 없게 될 수 있고, 유일한 해결 방법은 DB를 직접 수정하는 것뿐입니다. member.role로 감사 기록됩니다.
operationId setMemberRole
요청 본문: application/json · object
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 200 | 변경된 멤버 | Member |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
| 409 | 마지막 admin입니다(last_admin) | Error |
DELETE /v1/orgs/{orgSlug}/members/{userId}
다른 사람을 제거하려면 admin 이상 권한이 필요하지만, 자기 자신을 제거하는 것은 누구나 할 수 있습니다 (userId가 자신의 것이면 탈퇴가 됩니다). admin만 멤버를 제거할 수 있다면, viewer로 초대된 사람은 org를 떠날 방법이 없게 됩니다.
마지막 admin은 제거되거나 탈퇴할 수 없습니다 (409 last_admin). 감사 로그는 제거를 member.remove로, 탈퇴를 member.leave로 기록합니다 — 같은 문장이지만 이벤트가 다르므로 이름도 다릅니다.
operationId removeMember
| 상태 코드 | 설명 | 응답 본문 |
|---|---|---|
| 204 | 제거됨 | — |
| 403 | — | — |
| 404 | — | — |
| 409 | 마지막 admin입니다(last_admin) | Error |