← 목록
기타 2026-07-25 21KB 읽기 24분

크라우니클라우드(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)
  • GET: head(마지막 seq/anchor) 조회. ?verify=1 쿼리 시 검증모드(현재는 head 재확인과 동일 로직, intact:true,broken:[] 고정 — 실제 체인 재계산은 안 함, 주석에 "완전 재계산은 대용량에서 비실용"이라 명시).
  • 응답: {"service":"cloud","seq":N,"anchor":"<64hex>"} (verify 시 intact,broken 필드 추가)
  • POST: 본문 {"content":"<16~128자 hex>"} (최대 4096B). 새 anchor = SHA256(prev_anchor + content). 응답 {"ok":true,"seq":N,"anchor":"..."}.
  • 제네시스: 64자 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=aaaaaaaaaaaaaaaaaaaaaaaa404 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/verifycrowny_idX-Crowny-Owner로 격상하는 로직을 갖췄으나, cloud 서버(9611) 자신은 Bearer를 모른다 — JWT 검증은 항상 crowny.org 프록시 레이어(중간자)의 책임이고 cloud는 최종적으로 X-Crowny-Owner만 본다. 정본 문서화 목적상 명기.

    소비처 3+1곳 대조표

    #엔드포인트(소비처 관점)소비처실제 호출서버 정본 대응판정
    1GET/POST /api/cloud/fileswidget-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_v1CRITICAL-1 연장
    3POST /api/cloud/zipcloud/index.html(라이브 프로덕션 앱, "ZIP 다운로드" 버튼)/api/zip (세그먼트 1:1 일치라 리터럴 프록시로도 도달은 함)서버 상시 501 — 에러메시지의 "JS 원본 서버" 폴백이 cloud엔 없음(실측 확인)CRITICAL-2
    4POST /api/cloud/share {kind:'blob'}cloud/index.html(shareItem(), 모든 파일 공유 버튼)/api/share서버 kind!="note"는 상시 501, 마찬가지로 폴백 없음CRITICAL-2 연장
    5POST /api/cloud/merge 응답의 j.itemscloud/index.html(mergeSync())서버 POST 응답 {"ok":true}items 필드 부재 → 로컬 상태가 서버 병합결과로 절대 갱신 안 됨, 게다가 클라도 POST 전 원격 pull 없이 로컬 items 그대로 전송 → 사실상 "나중 POST가 이전 POST를 통짜로 덮어씀"(진짜 항목별 LWW 아님)CRITICAL-3
    6GET/POST /api/cloud/quota,store,merge,anchor,share,access (세그먼트명 1:1 일치 항목들)cloud/index.html리터럴 프록시로 정확히 대응 경로 도달정상일치
    7X-Crowny-Owner 헤더cloud-connect.js,widget-cloud.js,crowny-cloud.sh,cloud/index.html4곳 모두 정확히 X-Crowny-Owner(표준 대문자형) 사용서버가 리터럴 매칭하는 2가지 케이스(정확 X-Crowny-Owner: /소문자 x-crowny-owner: ) 중 하나와 항상 일치일치(단, 향후 신규 소비처가 혼합 대소문자로 보내면 놓칠 위험 — WARN-2)
    8Authorization: Bearer <JWT>cloud-connect.js(handle) 코드상 지원라이브 server.js 두 곳(crowny-ai/chansong) 모두 옛 인라인 프록시 사용 중 — fwdHeaders 화이트리스트에 authorization 없음cloud(9611) 자체도 Bearer 미지원(§인증계약)WARN-1(설계상 알려진 상태, P0-③ 대기)
    9POST /api/blob/upload/<owner>(URL 경로 owner, 헤더 없음)crowny-cloud.sh(up), cloud/index.html(업로드 XHR)서버 스펙과 정확히 일치(owner=URL 경로)일치일치
    10GET /api/blob/<owner>/<sha256>crowny-cloud.sh(down), cloud/index.html(blobURL())일치일치일치
    11GET /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(정보성)
    13Access-Control-Allow-Headers없음(CORS preflight는 브라우저가 자동)서버 CORS204 헤더 화이트리스트에 Authorization,Range 누락 (Content-Type, X-Crowny-Owner만)WARN-4 — Bearer 배선 시(WARN-1 해소 시점) 브라우저가 preflight에서 Authorization 헤더를 막을 수 있음. Range는 GET 다운로드용이라 보통 preflight 대상 아니지만 명시 누락
    14Access-Control-Allow-MethodsGET, 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 요약 + 권고 (코드 수정 없음 — 제안만)

    1. CRITICAL-1 (파일목록 API 불일치, 실측 재현): widget-cloud.js/cloud-connect.js.fetchRecentFiles()가 가리키는 파일목록 소스가 서버 실데이터 모델과 근본적으로 어긋남.
    - 권고: cloud-connect.jsupstreamPath() 별칭 테이블에서 files: '/api/store'files: '/api/merge?key=cloud_items_v1'로 교정하고, fetchRecentFiles()의 응답 파싱을 {"items": {...실제머지블록...}} 이중중첩(§3, CRITICAL-3과 함께 해소 필요) 구조에 맞게 재작성. 그 전에는 배선(배선-클라우드.md 배선B) 자체를 보류 권고 — 현재 미배선 상태(리터럴 프록시)라 실제 노출 피해는 아직 없지만, 위젯이 처음 마운트되는 순간 즉시 깨진다.
    1. CRITICAL-2 (ZIP/블롭공유 상시 501, 실측 재현): 프로덕션 앱(cloud/index.html)의 "ZIP 다운로드"·"공유 링크" 버튼이 항상 실패. 서버 에러 메시지가 존재하지 않는 JS 폴백을 언급해 디버깅을 오도.
    - 권고: (a) 단기 — 501 에러 메시지에서 "JS 원본 서버에서 처리" 문구 제거하고 "미구현"으로 정정(사용자 오인 방지), UI에서 해당 버튼 비활성화 또는 "준비 중" 안내로 임시 차단. (b) 중기 — zip은 CRC32 바이너리 직렬화를 한선씨 opcode로 포팅(블롭스트림.한선과 같은 계열), blob share는 이미 있는 blob 다운로드 opcode(856/859)를 재사용해 share 토큰→blob 매핑만 추가하면 note share와 동일 패턴으로 포팅 가능해 보임.
    1. CRITICAL-3 (merge 응답 items 부재, 진짜 LWW 미구현): 멀티기기 동기화가 "항목별 병합"이 아니라 "마지막 POST가 통짜로 이김" — 두 기기가 비슷한 시각에 다른 항목을 편집하면 한쪽이 유실.
    - 권고: (a) 서버 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/merge GET 응답 이중중첩(items.items), ?o= 쿼리 파라미터는 서버가 무시(헤더만 인정) — 둘 다 기능 장애는 아니나 API 형태 정리 대상.
    • WARN-4: CORS Access-Control-Allow-HeadersAuthorization,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 스펙 정본화) 밖이라 발견사항으로만 기록.