ZUKU API — Captcha

자체 호스팅 Proof-of-Work 캡차입니다. 외부 위젯(reCAPTCHA 등) 없이 CAPTCHA_HMAC_SECRET 기반 HMAC으로 챌린지를 발급·검증합니다.

가입·로그인 게이트와 디버그 검증 API를 포함합니다. 구현: backend/rs/src/captcha.rs, router.rs.

Base URL: https://zuzunza.com/api/v1 (로컬 http://localhost:3001/api/v1)

목차

  1. 개요 · 환경 변수
  2. Challenge
  3. Verify
  4. Auth 게이트 (zcaptcha_token)
  5. 클라이언트 풀이 흐름
  6. curl · JS/TS 예제
  7. 오류 코드
  8. 관련 문서

1. 개요 · 환경 변수

변수 기본 설명
CAPTCHA_HMAC_SECRET (빈 문자열) 필수(프로덕션). 비어 있으면 challenge/verify는 503 fail-closed
CAPTCHA_MAX_NUMBER 100000 PoW 탐색 상한(0..=maxnumber)
CAPTCHA_TTL_SECS 300 salt에 심는 만료(초)
CAPTCHA_DEV_BYPASS off 1이면 로컬 호스트 + 고정 바이패스 토큰 허용

프로토콜(레거시 servgate와 호환):

  1. 서버가 salt, challenge=hex(SHA-256(salt‖number)), signature=hex(HMAC-SHA256(secret, challenge)) 발급
  2. 클라이언트가 0..=maxnumber에서 challenge와 일치하는 number를 탐색
  3. base64(JSON{algorithm,challenge,number,salt,signature}) 토큰을 제출

내부 필터/샌드박스와 무관합니다. 캡차는 인증 남용 완화용입니다.


2. Challenge

POST /captcha/challenge

인증 불필요. 본문 없음.

성공 200data:

{
  "algorithm": "SHA-256",
  "challenge": "a1b2…",
  "salt": "zzcp_<hex>.<expiry_unix>",
  "signature": "…",
  "maxnumber": 100000
}
HTTP code 조건
503 CAPTCHA_NOT_CONFIGURED CAPTCHA_HMAC_SECRET 미설정
405 GET 등 비허용 메서드

3. Verify

POST /captcha/verify

서버-투-서버·디버그용. 가입/로그인 게이트는 본문 필드 zcaptcha_token으로 동일 검증기를 씁니다.

요청

{
  "token": "<solved-base64-token>",
  "dev_host": "localhost"
}

dev_hostCAPTCHA_DEV_BYPASS=1일 때만 의미가 있습니다.

성공 200data: { "success": true }

HTTP code 조건
503 CAPTCHA_NOT_CONFIGURED 시크릿 없음
403 CAPTCHA_FAILED 서명·만료·number·재사용 실패
400 BAD_REQUEST JSON 파싱 실패 등

4. Auth 게이트 (zcaptcha_token)

POST /auth/register · POST /auth/signup · POST /auth/login 공통:

서버 상태 동작
시크릿 없음 게이트 비활성 — 가입/로그인 막지 않음
시크릿 있음 본문 zcaptcha_token 필수. 실패 시 403 CAPTCHA_FAILED
CAPTCHA_DEV_BYPASS=1 + 허용 토큰/호스트 개발 우회

개발 바이패스 토큰(고정): __zuzunza_captcha_localhost_bypass__

(프로덕션에서 CAPTCHA_DEV_BYPASS를 켜지 마세요.)

자세한 가입/로그인 필드는 Authentication.


5. 클라이언트 풀이 흐름

POST /captcha/challenge
        │
        ▼
  worker: for n in 0..=maxnumber
            if sha256(salt + n) == challenge → found
        │
        ▼
  token = btoa(JSON.stringify({ algorithm, challenge, number, salt, signature }))
        │
        ▼
  POST /auth/register { …, zcaptcha_token: token }

권장:

  • UI 스레드를 막지 않도록 Web Worker에서 탐색
  • 실패 시 새 challenge를 다시 받고 재시도(만료·replay)
  • X-Request-ID로 문의 추적용 상관관계 유지

6. curl · JS/TS 예제

curl — challenge

curl -sS -X POST "https://zuzunza.com/api/v1/captcha/challenge" \
  -H "Accept-Language: ko-KR" \
  -H "X-Request-Id: $(uuidgen)"

curl — verify

curl -sS -X POST "https://zuzunza.com/api/v1/captcha/verify" \
  -H "Content-Type: application/json" \
  -d '{"token":"'"$TOKEN"'"}'

JS/TS — challenge → register

const API = "https://zuzunza.com/api/v1";

type Challenge = {
  algorithm: string;
  challenge: string;
  salt: string;
  signature: string;
  maxnumber: number;
};

async function fetchChallenge(): Promise<Challenge> {
  const res = await fetch(`${API}/captcha/challenge`, { method: "POST" });
  const env = await res.json();
  if (!env.success) throw new Error(`${env.error.code}: ${env.error.message}`);
  return env.data as Challenge;
}

/** 데모용 동기 풀이 — 프로덕션은 Worker로 이전하세요. */
function solvePoW(c: Challenge): string {
  // 실제 구현은 SHA-256(salt + String(n)) hex === c.challenge 일 때까지 n 증가
  throw new Error("implement PoW solver (see captcha.rs protocol)");
}

async function registerWithCaptcha(input: {
  email: string;
  password: string;
  handle: string;
}) {
  const challenge = await fetchChallenge();
  const zcaptcha_token = solvePoW(challenge);
  const res = await fetch(`${API}/auth/register`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "Accept-Language": "ko-KR" },
    body: JSON.stringify({ ...input, password_confirm: input.password, zcaptcha_token }),
  });
  const env = await res.json();
  if (!env.success) throw new Error(`${env.error.code}: ${env.error.message}`);
  return env.data;
}

더 많은 end-to-end 시나리오는 Examples.


7. 오류 코드

code HTTP 의미
CAPTCHA_NOT_CONFIGURED 503 HMAC 시크릿 미설정(challenge/verify)
CAPTCHA_FAILED 403 토큰 검증 실패 또는 게이트 거절
BAD_REQUEST 400 본문 형식 오류

8. 관련 문서