크라우니클라우드(cloud.crowny.org:9611) API 정본 v1
개요
P1-④(API 스펙 정본화). 정본 엔진은/Users/ef/crowny-services/서비스서버.한선(1172줄, hanseonc_high 컴파일 → 서비스서버.toau, SERVICE=cloud PORT=9611로 구동, blob 부분은 블롭스트림.한선이 blob_디스패처로 합류). 실측(2026-07-25~26): lsof -iTCP:9611 = crownyc run 서비스서버.toau 단일 프로세스. JS 원본(서비스서버.js)은 cloud용으로 기동되어 있지 않다 — node 인스턴스 3개는 각각 9942/9821/9860(다른 non-migrated 서비스)이고 cloud는 migrated.txt에 등재되어 100% 한선씨 컴파일 바이너리로만 서빙됨. 이 사실이 아래 CRITICAL-2의 근거.구 프로토타입 /Users/ef/crowny-cloud/server.js는 방치·아카이브 처리됨(2026-07-25 P0 완료, _방치아카이브_2026-07-25/) — 본 문서에서 완전히 무시.
정본 엔드포인트 레퍼런스
모든 응답은 Connection: close(Content-Length 미사용, VM 문자열 65535B 캡 회피). owner 스코핑 API는 X-Crowny-Owner 헤더(8~100자, [A-Za-z0-9_-]만 허용, 소유자유효() 서비스서버.한선:369-386)를 요구하며 없거나 형식위반이면 401.
1. GET/POST /api/store — owner 스코핑 JSON 스냅샷
핸들러: store처리() 서비스서버.한선:480-524, 디스패치 :1056-1060.
- 저장 경로:
data/store/<owner>/<서비스명>.json(여기선<서비스명>=cloud) - GET: 헤더
X-Crowny-Owner필수. 파일 있으면 raw 내용 그대로 200 JSON, 없으면{}. - POST: 헤더
X-Crowny-Owner필수, 본문 = 임의 JSON(최대 512KB,본문읽기()내부에서 재차 64000바이트 초과 즉시 거부·65535B VM 캡 회피). 원자쓰기(.tmp+mv -f). 응답{"ok":true}. - 실측:
$ curl -s http://127.0.0.1:9611/api/store -H "X-Crowny-Owner: anon_testowner1"
{}
$ curl -s http://127.0.0.1:9611/api/quota
{"error":"no owner"}
- 실제 데이터 형태(라이브 샘플):
data/store/u_.../cloud.json= 브라우저 localStorage 통짜 미러({"crowny-auth":"...", "cloud_items_v1":"[...]"(문자열!), "crowny-theme":"light", "crowny-convs":"[...]"}) — 구조화된 "파일 목록" API가 아니라 옵션 키-값 블롭 저장소. 실제 파일 목록은 §3/api/merge?key=cloud_items_v1에 있음(→ CRITICAL-1 근거).
2. GET/POST /api/anchor — 블록체인 앵커 저널(해시체인)
핸들러: anchor처리() :529-637, 디스패치 :1064-1068.
data/anchor/<owner>/<서비스명>.jsonl (append-only)?verify=1 쿼리 시 검증모드(현재는 head 재확인과 동일 로직, intact:true,broken:[] 고정 — 실제 체인 재계산은 안 함, 주석에 "완전 재계산은 대용량에서 비실용"이라 명시).{"service":"cloud","seq":N,"anchor":"<64hex>"} (verify 시 intact,broken 필드 추가){"content":"<16~128자 hex>"} (최대 4096B). 새 anchor = SHA256(prev_anchor + content). 응답 {"ok":true,"seq":N,"anchor":"..."}.0 반복.curl http://127.0.0.1:9611/api/anchor -H "X-Crowny-Owner: anon_testowner1" → {"service":"cloud","seq":0,"anchor":"000...0"}3. GET/POST /api/merge — LWW(Last-Write-Wins) 항목별 머지
핸들러: merge처리() :642-689, 디스패치 :1072-1076.
- 저장:
data/merge/<owner>/<key>.json - GET
?key=<키>:key필수(없으면 400). 파일 있으면{"items": <파일 통짜 내용>}로 래핑(⚠️ 서버 POST가 바디 전체를 그대로 저장하므로, 클라 POST 바디가{key,items,deleted}형태면 GET 응답은{"items":{"key":...,"items":[...],"deleted":[...]}}이중 중첩이 됨 — WARN-3 참조). 없으면{"items":[]}. - POST: 본문에
key필수(최대 4MB). 바디 전체를 파일에 그대로 원자쓰기(서버는 실제 LWW 병합을 하지 않음 — 주석 원문: "LWW 세부 로직은 클라이언트 측이 책임(서버=단순 상태저장소)"). 응답{"ok":true}만 반환, 머지 결과 items는 응답에 없음(→ CRITICAL-3). - 실측:
data/merge/anon_.../cloud_items_v1.json라이브 샘플 ={"key":"cloud_items_v1","items":[],"deleted":[]}.
4. GET /api/quota — 블롭 쿼터 조회
핸들러: quota처리() :693-720, 디스패치 :1080-1084.
data/blobs/<owner>디렉토리 크기를du -sb(임시파일 경유)로 집계.- 응답:
{"used":N,"quota":268435456,"maxFile":67108864}(쿼터 256MB 고정, 파일당 상한 64MB 고정 — 코드 하드코딩, 과금 연동 전 정적값) - 실측:
{"used":0,"quota":268435456,"maxFile":67108864}
5. GET /api/access — 접근 이력 조회
핸들러: access처리() :724-783, 디스패치 :1088-1092.
- 소스:
data/access/<owner>.jsonl(up/down 이벤트 라인) ?id=<필터>옵션. 응답:{"up":[{ts,ip}...],"down":[...],"upCount":N,"downCount":N}(up/down 각 최대 50개 노출, count는 전체)- 파일 없으면
{"up":[],"down":[],"upCount":0,"downCount":0}.
6. GET /api/status — ecosystem 전체 서비스 헬스체크 (ecosystem 서비스 전용)
핸들러: status처리() :787-839, 디스패치 :1096-1100. 서비스명 == "ecosystem"일 때만 라우팅됨 — cloud에서는 호출 불가(404 아님, 그냥 이 분기 자체가 안 걸리고 정적서빙/랜딩으로 폴백). registry.psv 전 서비스에 순차 TCP GET /api/health 프로브(자기서명 소켓, 헬스응답에 "200" 포함되면 live). 참고용으로만 기재 — cloud API 스펙에는 실질 미포함.7. POST /api/구매확정 (별칭 /api/purchase-confirm) — 크라우니코인 적립
핸들러: 구매확정처리() :843-896, 디스패치 :1102-1112.
- 본문
{"account"|"phone":..., "amount":N}.서비스명=="beauti"일 때만 실제 적립(그 외는{"ok":false,"message":"이 서비스는 코인 적립 대상이 아닙니다."}) → cloud에서는 항상 비대상 응답. verse(:9561) TCP 중계 로직 존재하나 cloud엔 무관. 참고용 기재.
8. GET/POST/DELETE /api/share — 공유 링크
핸들러: share처리() :902-1005, 디스패치 :1114-1119.
- 저장:
data/share/<24~128자 토큰>.json(토큰=SHA256(owner+ts)앞 24자,토큰유효():393-410 화이트리스트 검증, 2026-07-25 P0 강화) - GET
?t=<토큰>: 인증 불요(공개 링크).kind=="note"면 평문(text/plain) 200 반환.kind=="blob"이면 501("blob-share-range: JS 원본 서버에서 처리" — §CRITICAL-2, 이 폴백은 실재하지 않음). 만료(exp) 지났으면 파일 삭제 후 410. 토큰 형식오류 400, 파일없음 404. - DELETE
?t=<토큰>:X-Crowny-Owner필수, 저장된owner필드와 불일치 시 403. - POST:
X-Crowny-Owner필수, 본문 최대 256KB.kind!="note"(즉"blob"포함 전부)면 501("blob-share-create: JS 원본 서버에서 처리" — 마찬가지로 실재하지 않는 폴백, §CRITICAL-2).kind=="note"만 실제 생성됨:{"ok":true,"token":"<24자>"}. - 실측: 존재하지 않는 토큰
GET /api/share?t=aaaaaaaaaaaaaaaaaaaaaaaa→404 Not Found(본문 없음 — 응답404() 텍스트 핸들러가 plain "Not Found").
9. POST /api/blob/upload/<owner> — blob 업로드
핸들러: blob_업로드()(블롭스트림.한선:405-563), 디스패치 blob_디스패처():600-687(서비스서버.한선:1122-1129에서 위임).
- ★ 인증 모델이 다르다 — X-Crowny-Owner 헤더가 아니라 URL 경로 세그먼트에서 owner 추출(
blob소유자안전():581-598, 동일 화이트리스트[A-Za-z0-9_-], 1~100자). Content-Length헤더 필수(없으면 411), 64MB 초과 시 413.- 버퍼 기반 NUL-safe 수신 →
SHA256계산 →data/blobs/<owner>/<sha256>저장(sha256 파일명이므로 content-addressed dedup 자동). - 응답:
{"id":"<64hex sha256>","size":N,"dedup":true|false}
10. GET /api/blob/<owner>/<sha256> — blob 다운로드 (전체 또는 Range)
핸들러: blob_다운로드()/blob_range()(블롭스트림.한선:227-335), 디스패치 동일.
- owner도 sha256(64자hex 강제)도 URL 경로에서 추출, 인증 헤더 불요(콘텐츠해시 자체가 사실상의 캡+공개 URL 모델 — 임의접근 가능하다는 뜻이므로 사장님 검토 필요할 수 있음, 별건).
Range: bytes=start-end헤더 있으면 206 Partial Content(Content-Range,Accept-Ranges: bytes포함), 없으면 200 전체.- 파일 크기는
wc -c로 구함(대용량도 정확).
11. POST /api/zip — ZIP 다운로드 (미포팅, 항상 501)
디스패치 :1132-1136. 무조건 {"error":"zip: CRC32 바이너리 직렬화는 JS 원본 서버에서 처리","note":"JS 원본 서버에서 처리"} 반환. 이 "JS 원본 서버" 폴백은 cloud에 존재하지 않는다(§CRITICAL-2).12. GET /api/health, /health — 헬스체크
디스패치 :1035-1040. {"status":"ok","service":"cloud","domain":"cloud.crowny.org","port":9611,"engine":"hanseon"}13. GET /crowny-tokens.css — 디자인 토큰 SSOT 프록시
디스패치 :1042-1052. public/_shared/crowny-tokens.css 서빙.14. OPTIONS * — CORS preflight
디스패치 :1029-1033. Access-Control-Allow-Origin: *, Methods GET,POST,DELETE,OPTIONS(⚠️ PUT 없음 — 아래 WARN 참조), Headers Content-Type, X-Crowny-Owner(⚠️ Authorization·Range 없음).15. 정적파일 서빙 + 랜딩 폴백
디스패치 :1138-1160.public/cloud/* 우선 서빙(index.html 폴백), 없으면 모노톤 랜딩 HTML.인증 계약
- 현행(라이브):
X-Crowny-Owner: <8~100자 [A-Za-z0-9_-]>헤더. 무비밀번호 — 형식만 맞으면 통과하는 "실질적 bearer 토큰" 모델(2026-07-24 현황분석 문서 실증). blob 업/다운로드만 예외로 URL 경로 기반. - P0-③ 이행 예정: 실인증(비번/세션/2FA) 3단계 마이그레이션 설계 완료(
2026-07-25-크라우니클라우드-실인증-설계.md), 구현 미착수.cloud-connect.js(crowny.org 프록시 계층)는 이미Authorization: Bearer <JWT>→auth.crowny.org:9401 /api/auth/verify→crowny_id→X-Crowny-Owner로 격상하는 로직을 갖췄으나, cloud 서버(9611) 자신은 Bearer를 모른다 — JWT 검증은 항상 crowny.org 프록시 레이어(중간자)의 책임이고 cloud는 최종적으로 X-Crowny-Owner만 본다. 정본 문서화 목적상 명기.
소비처 3+1곳 대조표
| # | 엔드포인트(소비처 관점) | 소비처 | 실제 호출 | 서버 정본 대응 | 판정 |
|---|---|---|---|---|---|
| 1 | GET/POST /api/cloud/files | widget-cloud.js(crowny-ai/public), cloud-connect.js(fetchRecentFiles()) | 라이브 server.js(crowny-ai·chansong 동일 인라인 블록, cloud-connect.js handle() 미배선)가 리터럴 /api/cloud/X→/api/X 1:1 프록시 → /api/files로 감 | 그런 라우트 없음(랜딩 HTML 200 text/html로 폴백) | CRITICAL-1 |
| 2 | 〃 (cloud-connect.js 자체 별칭, 아직 미배선) | cloud-connect.js upstreamPath(): files→/api/store | 배선 완료되어도 /api/store를 침 | /api/store는 파일목록이 아니라 localStorage 통짜 미러(§1). 실제 파일목록은 /api/merge?key=cloud_items_v1 | CRITICAL-1 연장 |
| 3 | POST /api/cloud/zip | cloud/index.html(라이브 프로덕션 앱, "ZIP 다운로드" 버튼) | /api/zip (세그먼트 1:1 일치라 리터럴 프록시로도 도달은 함) | 서버 상시 501 — 에러메시지의 "JS 원본 서버" 폴백이 cloud엔 없음(실측 확인) | CRITICAL-2 |
| 4 | POST /api/cloud/share {kind:'blob'} | cloud/index.html(shareItem(), 모든 파일 공유 버튼) | /api/share | 서버 kind!="note"는 상시 501, 마찬가지로 폴백 없음 | CRITICAL-2 연장 |
| 5 | POST /api/cloud/merge 응답의 j.items | cloud/index.html(mergeSync()) | 서버 POST 응답 {"ok":true}만 | items 필드 부재 → 로컬 상태가 서버 병합결과로 절대 갱신 안 됨, 게다가 클라도 POST 전 원격 pull 없이 로컬 items 그대로 전송 → 사실상 "나중 POST가 이전 POST를 통짜로 덮어씀"(진짜 항목별 LWW 아님) | CRITICAL-3 |
| 6 | GET/POST /api/cloud/quota,store,merge,anchor,share,access (세그먼트명 1:1 일치 항목들) | cloud/index.html | 리터럴 프록시로 정확히 대응 경로 도달 | 정상 | 일치 |
| 7 | X-Crowny-Owner 헤더 | cloud-connect.js,widget-cloud.js,crowny-cloud.sh,cloud/index.html | 4곳 모두 정확히 X-Crowny-Owner(표준 대문자형) 사용 | 서버가 리터럴 매칭하는 2가지 케이스(정확 X-Crowny-Owner: /소문자 x-crowny-owner: ) 중 하나와 항상 일치 | 일치(단, 향후 신규 소비처가 혼합 대소문자로 보내면 놓칠 위험 — WARN-2) |
| 8 | Authorization: Bearer <JWT> | cloud-connect.js(handle) 코드상 지원 | 라이브 server.js 두 곳(crowny-ai/chansong) 모두 옛 인라인 프록시 사용 중 — fwdHeaders 화이트리스트에 authorization 없음 | cloud(9611) 자체도 Bearer 미지원(§인증계약) | WARN-1(설계상 알려진 상태, P0-③ 대기) |
| 9 | POST /api/blob/upload/<owner>(URL 경로 owner, 헤더 없음) | crowny-cloud.sh(up), cloud/index.html(업로드 XHR) | 서버 스펙과 정확히 일치(owner=URL 경로) | 일치 | 일치 |
| 10 | GET /api/blob/<owner>/<sha256> | crowny-cloud.sh(down), cloud/index.html(blobURL()) | 일치 | 일치 | 일치 |
| 11 | GET /api/cloud/quota 응답 파싱 | widget-cloud.js,cloud-connect.js.fetchQuota() | q.used,q.quota 필드 직접 읽음 | 서버 응답 {"used","quota","maxFile"}과 필드명 일치 | 일치 |
| 12 | ?o=<owner> 쿼리 병기 | widget-cloud.js(apiFetch), cloud-connect.js(fetchQuota/fetchRecentFiles) | 헤더와 쿼리 동시 전송 | 서버는 쿼리 o=를 전혀 안 읽음(헤더만) — 해는 없지만 죽은 파라미터 | WARN-3(정보성) |
| 13 | Access-Control-Allow-Headers | 없음(CORS preflight는 브라우저가 자동) | — | 서버 CORS204 헤더 화이트리스트에 Authorization,Range 누락 (Content-Type, X-Crowny-Owner만) | WARN-4 — Bearer 배선 시(WARN-1 해소 시점) 브라우저가 preflight에서 Authorization 헤더를 막을 수 있음. Range는 GET 다운로드용이라 보통 preflight 대상 아니지만 명시 누락 |
| 14 | Access-Control-Allow-Methods | — | — | GET, POST, DELETE, OPTIONS뿐, PUT 없음 | 정보성(현재 PUT 쓰는 소비처 없음) |
| 15 | /api/cloud/access,/api/cloud/status,/api/cloud/구매확정 | 미사용 | cloud-connect.js/widget-cloud.js/crowny-cloud.sh 3곳 모두 호출 안 함(cloud/index.html만 access 사용) | — | 정보성 |
chansong 드리프트 결론
/Users/ef/chansong.crowny.org/에서 cloud 관련 파일 4종 발견:
engine/cloud-connect.js, public/widget-cloud.js, public/cloud/index.html, server.js의 /api/cloud/* 프록시 블록.전부 crowny-ai 원본과 바이트 단위로 완전 동일(diff exit 0, 4파일 전수). server.js 프록시 블록도 diff 결과 완전 일치(주변 /api/play/* 블록의 위치 차이만 있고 cloud 블록 자체는 동일). 드리프트 없음 — 두 저장소가 같은 소스에서 동기화된 상태 유지 중.
참고: /Users/ef/crowny-services/public/cloud/index.html(cloud 서버가 직접 서빙하는 버전)은 crowny-ai/chansong 판과 의도적으로 다름 — 경로 접두사가 /api/cloud/*가 아니라 /api/*(same-origin이라 프록시 접두 불필요)뿐이고 그 외 로직은 동일. 이건 드리프트가 아니라 배포 컨텍스트 차이(직접서빙 vs crowny.org 임베드).
CRITICAL 요약 + 권고 (코드 수정 없음 — 제안만)
- CRITICAL-1 (파일목록 API 불일치, 실측 재현):
widget-cloud.js/cloud-connect.js.fetchRecentFiles()가 가리키는 파일목록 소스가 서버 실데이터 모델과 근본적으로 어긋남.
cloud-connect.js의 upstreamPath() 별칭 테이블에서 files: '/api/store' → files: '/api/merge?key=cloud_items_v1'로 교정하고, fetchRecentFiles()의 응답 파싱을 {"items": {...실제머지블록...}} 이중중첩(§3, CRITICAL-3과 함께 해소 필요) 구조에 맞게 재작성. 그 전에는 배선(배선-클라우드.md 배선B) 자체를 보류 권고 — 현재 미배선 상태(리터럴 프록시)라 실제 노출 피해는 아직 없지만, 위젯이 처음 마운트되는 순간 즉시 깨진다.
- CRITICAL-2 (ZIP/블롭공유 상시 501, 실측 재현): 프로덕션 앱(
cloud/index.html)의 "ZIP 다운로드"·"공유 링크" 버튼이 항상 실패. 서버 에러 메시지가 존재하지 않는 JS 폴백을 언급해 디버깅을 오도.
- CRITICAL-3 (merge 응답 items 부재, 진짜 LWW 미구현): 멀티기기 동기화가 "항목별 병합"이 아니라 "마지막 POST가 통짜로 이김" — 두 기기가 비슷한 시각에 다른 항목을 편집하면 한쪽이 유실.
merge처리() POST에서 저장 직전 기존 파일을 읽어 id 기준 실제 LWW 병합(양쪽 items를 id별 최신 ts 승자로 합치고 deleted 툼스톤 적용) 후 병합결과를 응답 {"ok":true,"items":[...]}으로 반환. (b) 클라 mergeSync()도 POST 전 최신 서버 상태를 한 번 GET(또는 서버가 병합해 돌려주는 응답을 신뢰)해 사용자에게 보여지는 로컬 items를 갱신. 데이터 모델 변경(서버가 상태 저장소→실제 병합기)이라 P1-⑥(데이터 정리) 이후, 기존 data/merge/*.json(통짜 POST바디 형태)과의 하위호환 마이그레이션 필요.WARN 요약
- WARN-1: Bearer/JWT 경로 전체 미배선(cloud-connect.js 코드는 있으나 라이브 서버가 옛 리터럴 프록시 사용, cloud 자체도 X-Crowny-Owner만 이해). P0-③ 실인증 설계 완료·구현 대기 — 알려진 상태, 별도 이슈 아님.
- WARN-2: X-Crowny-Owner 헤더 대소문자 리터럴 매칭 2종만 지원. 현재 소비처 전부 표준형이라 무해하나 신규 소비처 작성 시 유의.
- WARN-3:
/api/mergeGET 응답 이중중첩(items.items),?o=쿼리 파라미터는 서버가 무시(헤더만 인정) — 둘 다 기능 장애는 아니나 API 형태 정리 대상. - WARN-4: CORS
Access-Control-Allow-Headers에Authorization,Range누락 — WARN-1(Bearer) 해소 시 브라우저 preflight가 막을 수 있음. 배선 시점에 함께 교정 권고.
관련 파일
- 서버 정본:
/Users/ef/crowny-services/서비스서버.한선(1172줄),/Users/ef/crowny-services/블롭스트림.한선(691줄) - 실행:
/Users/ef/crowny-services/서비스서버.toau(hanseonc_high 컴파일),manage.sh,com.crowny.services.plist - CLI:
/Users/ef/crowny-services/crowny-cloud.sh+ 동반crowny-cloud.한선 - 소비처:
/Users/ef/crowny-ai/engine/cloud-connect.js,/Users/ef/crowny-ai/public/widget-cloud.js,/Users/ef/crowny-ai/public/cloud/index.html(프로덕션 앱, cloud/index.html),/Users/ef/crowny-ai/배선-클라우드.md(runbook, 미적용) - chansong 복제본(드리프트 없음):
/Users/ef/chansong.crowny.org/engine/cloud-connect.js,public/widget-cloud.js,public/cloud/index.html - cloud 자체 서빙판:
/Users/ef/crowny-services/public/cloud/index.html(경로접두만 다름, 의도적) - 선행 문서:
2026-07-24-크라우니클라우드-업그레이드-현황분석-작업목록.md(P0~P2 전체 목록),2026-07-25-크라우니클라우드-백업설계-P2프레이밍.md
잔여 이슈
- CRITICAL-1/2/3은 코드 수정 없이 "권고"만 제시함(작업 지시 범위 = 문서화). 실제 수정은 별도 작업으로 승인 필요.
data/merge/디렉토리에 SQLi 페이로드가 파일명으로 남아있는 테스트 오염 잔존 확인(예:cloud_items_v1ovIt8P6s')) OR 76=(SELECT 76 FROM PG_SLEEP(15))--.json) — 이미 P1-⑥(데이터 정리) 항목으로 별도 등재되어 있어 본 문서에서는 재기재만 하고 손대지 않음./api/blob/<owner>/<sha256>다운로드가 무인증 공개 URL 모델인 점은 P0-③(실인증) 논의에 함께 들어가야 할 사안으로 보이나, 이번 작업 범위(API 스펙 정본화) 밖이라 발견사항으로만 기록.