콘텐츠로 이동

HTTP API

기본 주소는 http://localhost:3000입니다. 모든 응답은 JSON이며 세션은 HttpOnly 쿠키 umul_session으로 유지합니다.

인증

POST /v1/auth/nonce

{ "address": "0xEf792c4973Cc8ef6aF433B3bd9f4461B701301f6" }

응답은 서명할 SIWE 메시지와 만료 시각입니다. nonce는 5분 후 만료하고 한 번만 사용할 수 있습니다. 저장되는 값은 원문이 아니라 SHA-256 해시입니다.

{ "message": "localhost:5173 wants you to sign in ...", "expiresAt": "..." }

POST /v1/auth/verify

{ "message": "<위에서 받은 메시지>", "signature": "0x..." }

서명에서 복구한 주소가 메시지의 주소와 다르면 거부합니다. 성공하면 Set-Cookie: umul_session=...; HttpOnly; SameSite=Lax를 내려줍니다.

DELETE /v1/auth/session

세션을 폐기하고 쿠키를 만료시킵니다. 세션이 없어도 204를 반환합니다.

오류 코드 상태
INVALID_REQUEST 400
INVALID_WALLET_ADDRESS, INVALID_LOGIN_MESSAGE, LOGIN_MESSAGE_MISMATCH 400
INVALID_WALLET_SIGNATURE, LOGIN_NONCE_UNKNOWN 401
LOGIN_ORIGIN_NOT_ALLOWED 403
LOGIN_NONCE_ALREADY_USED 409
LOGIN_NONCE_EXPIRED 410

INVALID_REQUEST는 요청 본문이 스키마에 맞지 않을 때입니다.

증빙

모든 증빙 엔드포인트는 유효한 세션을 요구합니다. 세션이 없으면 401 SESSION_REQUIRED를 반환합니다. 참여자가 아닌 주소에는 존재 여부를 알리지 않고 404 EVIDENCE_REQUEST_NOT_FOUND를 반환합니다.

POST /v1/evidence-requests

Idempotency-Key 헤더가 필요합니다. 같은 키로 다시 요청하면 새 요청을 만들지 않고 기존 요청을 돌려줍니다.

{
  "name": "계약 조건 합의",
  "messageRefs": ["<xmtp-message-id>", "..."],
  "evidenceId": "0x...",
  "contentHash": "0x...",
  "conversationIdHash": "0x...",
  "participantsHash": "0x...",
  "manifestVersion": "gasok-evidence-v1",
  "messageCount": 12,
  "conversationStartedAt": "2026-07-26T00:00:00.000Z",
  "conversationEndedAt": "2026-07-26T01:00:00.000Z",
  "participants": ["0x...", "0x..."]
}

모든 필드가 필수이고 정의되지 않은 필드는 거부합니다.

필드 제약
name 1~100자. 온체인에 올라가지 않는 사람이 읽는 라벨
messageRefs XMTP 메시지 ID 1~4096개. 본문은 받지 않습니다. 개수가 messageCount와 같아야 합니다
manifestVersion 1~64자
messageCount 1 이상
participants 2~32개 주소

세션 주소가 participants에 없으면 403입니다. 메시지 원문은 받지 않습니다.

GET /v1/evidence-requests

세션 주소가 참여자인 요청만 최신순으로 반환합니다. 저장한 것이 먼저 옵니다.

GET /v1/evidence-requests/:id

응답에는 name, messageRefs와 함께 saved가 들어갑니다. saved는 요청한 참여자 본인의 저장 여부이므로 같은 증빙이라도 사람마다 값이 다릅니다. 서명과 참여자 주소는 노출하되 세션 토큰과 메시지 원문은 담지 않습니다.

POST /v1/evidence-requests/:id/save

증빙을 저장(즐겨찾기)하거나 해제합니다. 참여자별 상태라 한 사람이 저장해도 다른 참여자의 목록에는 영향이 없습니다.

{ "saved": true }

POST /v1/evidence-requests/:id/signatures

{ "signature": "0x<130 hex>" }

서명자는 세션 주소로 결정합니다. 요청 본문에 주소를 넣을 수 없습니다.

POST /v1/evidence-requests/:id/rejection

POST /v1/evidence-requests/:id/submission

{ "transactionHash": "0x...", "outcome": "confirmed", "blockNumber": 31763659 }

outcome을 생략하면 SUBMITTED까지만 기록합니다.

POST /v1/evidence-requests/:id/archive

{ "ciphertext": "<base64>" }

증빙 원문의 아카이브 업로드입니다. 클라이언트가 AES-256-GCM 으로 잠근 암호문만 받고, 콘텐츠 키는 XMTP 제어 메시지(E2EE)로만 오가므로 서버는 열 수 없습니다. 제안자가 아니면 403 EVIDENCE_ARCHIVE_FORBIDDEN입니다. 최초 1회만 저장되고, 같은 내용의 재시도는 성공, 다른 내용은 409 EVIDENCE_ARCHIVE_CONFLICT다. 암호문은 GCM 봉투 최소 길이(29바이트)에서 2MB까지 받습니다.

POST /v1/evidence-requests/:id/archive/keys

{ "wrappedKey": "<base64 80자>" }

서명을 마친 참여자가 자신의 래핑된 콘텐츠 키(60바이트 고정)를 맡깁니다. 래핑 키는 지갑 서명에서 파생한 키로 잠근 값이라 서버는 열 수 없습니다. 서명 전에는 403 EVIDENCE_ARCHIVE_FORBIDDEN입니다. 저장 규칙은 archive 와 같다 (first-write-wins, 다른 값 409).

GET /v1/evidence-requests/:id/archive

{ "ciphertext": "<base64>", "wrappedKey": "<base64|null>" }

서명한 참여자만 암호문과 자신의 래핑 키를 받습니다. 서명하지 않은 참여자와 아카이브가 없는 요청에는 404 EVIDENCE_ARCHIVE_NOT_FOUND를 반환해 존재 여부를 알리지 않습니다. wrappedKey가 null 이면 키 등록에 실패한 서명자입니다.

증빙 상태

PROPOSED -> SIGNING -> READY -> SUBMITTED -> CONFIRMED
     |          |         |          |
     +------> REJECTED    +------> FAILED
     +------> EXPIRED

FAILED에서는 수집한 서명을 유지한 채 재제출할 수 있습니다.

오류 코드 상태
INVALID_REQUEST, IDEMPOTENCY_KEY_REQUIRED 400
EVIDENCE_HASH_MALFORMED, EVIDENCE_SIGNATURE_MALFORMED, EVIDENCE_ARCHIVE_MALFORMED 400
EVIDENCE_NAME_INVALID, EVIDENCE_MESSAGE_REFS_MISMATCH, EVIDENCE_TIME_RANGE_INVALID 400
EVIDENCE_PARTICIPANTS_TOO_FEW, EVIDENCE_PARTICIPANTS_DUPLICATED 400
EVIDENCE_REQUESTER_NOT_PARTICIPANT 400
SESSION_REQUIRED 401
EVIDENCE_PARTICIPANT_UNKNOWN, EVIDENCE_ARCHIVE_FORBIDDEN 403
EVIDENCE_REQUEST_NOT_FOUND, EVIDENCE_ARCHIVE_NOT_FOUND 404
EVIDENCE_SIGNATURE_ALREADY_SUBMITTED, EVIDENCE_ARCHIVE_CONFLICT 409
EVIDENCE_REQUEST_NOT_READY, EVIDENCE_REQUEST_NOT_SIGNABLE 409
EVIDENCE_REQUEST_NOT_REJECTABLE, EVIDENCE_REQUEST_NOT_SUBMITTED 409
EVIDENCE_REQUEST_EXPIRED 410

EVIDENCE_REQUESTER_NOT_PARTICIPANT은 403이 아니라 400입니다. 제안자가 자신을 participants에 넣지 않은 것은 권한 문제가 아니라 잘못된 요청입니다.

헬스체크

GET /health는 { "status": "ok", "time": "<ISO 8601>" }을 반환합니다.