인터페이스 계약 매트릭스 — entitlement 클레임, 오류 코드, 활성화 경로
읽기 안내
SDK와 Worker 사이의 사용권 필드·오류 코드·활성화 경로 비교표입니다.
클라이언트 업데이트 시 issuer, 활성화 경로, Claim, 오류 매핑을 확인하세요. wire 버전 협상은 아직 없습니다.
상태:
VERIFIED— 모든 셀이 d1 커밋(e24890a7+ d1 변경)의 구현을 인용합니다. 이것은@yetdapay/license-*SDK를 호스티드 DevPay 백엔드(be-cloudflareWorker) 또는 자체 호스팅@yetdapay/license-server와 페어링하기 위한 단일 계약 레퍼런스입니다.Wire 계약 버전: v1(암묵적 — 버전 필드나 협상이 존재하지 않음;
known-limitations.md§3.2 참조).
1. Entitlement 클레임
컬럼: Worker actual = be-cloudflare가 발급/검증하는 것 (license-crypto/src/lib.rs, src/license_security.rs) · SDK default = @yetdapay/license-server 기본값 (sdk/license/server/src/server.mjs:29-35, entitlement.mjs) · SDK worker-preset = createWorkerConfig() (d1).
| 클레임 | Worker actual | SDK default | SDK worker 프리셋 |
|---|---|---|---|
iss | "devpay-lic"(verify 시 필수; license-crypto/lib.rs:28,59) | "yp" 발급 + 필수(server.mjs:30) | "devpay-lic"(WORKER_ISSUER) |
aud | 없음 — 발급도 검사도 안 함 | audience 설정이 아니면 없음; 설정 시 발급 + 필수 | 없음 — 프리셋은 audience 미설정 유지. Worker aud 발급은 DEFERRED(§5, known-limitations.md §3.8) |
lic_id | 필수 문자열(없으면 verify가 fail closed) | 필수 문자열 | 동일 |
product_id | 필수; 라이브 라이선스 행과 재검사(드리프트 시 401) | 필수; 저장소와 재검사(server.mjs:403) | 동일 |
tenant_id | 없음 — DEFERRED(known-limitations.md §3.8) | 항상 발급(nullable); expectedTenantId를 통해서만 강제 | 없음 허용(null로 정규화) |
features | 상품 네임스페이스에서 발급; 없으면 [], 비문자열 항목 드롭 | 필수 문자열 배열(잘못된 형태 → UNAUTHORIZED) | 동일 |
jkt_bound | 발급 + 필수; 증명 jwk에 바인딩 | 필수(cnf.jkt로 폴백) | 동일 |
cnf.jkt | 발급(jkt_bound와 동일 값) | 발급 + 둘 다 있으면 jkt_bound와 교차 검사 | 동일 |
iat | 발급; Worker에서 검증 안 함(exp만 검사) | 필수 숫자; 60초 skew 초과 미래 iat 거부 | 동일 |
exp | iat + 120(ENTITLEMENT_TTL_SEC); exp < now 거부 | 필수; exp <= now 거부; 기본 TTL 120초 | 동일 |
노트:
jkt=sha256(sha256(RFC 7638 thumbprint))— DevPay 고유 이중 해시, RFC 9449 thumbprint가 아님 (experiments/secure-browser-licensing/tools/sdk-worker-contract.test.mjs).- Worker는 모든 PoP 엔드포인트를
LICENSE_SECURITY_ENABLED뒤에 기능 게이트; 없으면404(be-cloudflare/src/lib.rs:76-94,239). - Axum
be/에는 PoP entitlement 플레인이 없음 — 이 클레임을 발급하지도 검증하지도 않음(레거시 라이선스 레코드만, §3).
2. 오류 코드
컬럼: Axum be/(레거시 라이선스 표면 + 일반 err() 코덱) · Worker(license_security.rs) · SDK(@yetdapay/license-serverErrorCode→HTTP; 웹 클라이언트는 {code}를 그대로 전달).
| 실패 | Axum be/ | Worker | SDK (@yetdapay/license-server) |
|---|---|---|---|
| 잘못된 body / 누락 필드 | 400 BAD_JSON/BAD_MSGPACK/BAD_PROTOBUF(코덱) · 400 VALIDATION(필드 검사) | 400 VALIDATION | 400 VALIDATION |
잘못된 public_jwk | n/a(PoP 플레인 없음) | 400 VALIDATION("invalid public_jwk") | 400 INVALID_PUBLIC_JWK — 별개 코드 |
| 알 수 없는 라이선스 키(활성화) | 200 body {code:"NOT_FOUND", valid:false}(verify); 404 NOT_FOUND(레코드 라우트) | 403 NOT_FOUND("license not activatable") | 404 NOT_FOUND |
| 상품 불일치 | 200 body PRODUCT_MISMATCH | 403 PRODUCT_MISMATCH | 403 PRODUCT_MISMATCH |
| revoked / suspended / expired | 200 body 코드(valid=false) | 403 동일 코드 † | 403 동일 코드 |
알 수 없는 라이선스 status | 200 body INVALID_STATUS | 403 INVALID_STATUS | 403 REVOKED(알 수 없는 상태는 신뢰 안 함) |
| 디바이스 제한 | 409 DEVICE_LIMIT(/licenses/:id/activations) | 403 DEVICE_LIMIT | 403 DEVICE_LIMIT |
| 인증 실패(토큰/증명/바인딩/리플레이/body 해시) | 401 일반 | 401 UNAUTHORIZED — 모든 검사에 하나의 불투명 코드 | 401 UNAUTHORIZED — 동일 불투명 정책 |
| entitlement에 없는 기능 | n/a | 403 FORBIDDEN | 403 FORBIDDEN |
| 무효 capability(알 수 없음/불일치) | n/a | 403 FORBIDDEN("invalid capability") | 403 CAPABILITY_INVALID — 별개 코드 |
| capability 소비됨/만료 | n/a | 409 CAPABILITY_CONSUMED | 409 CAPABILITY_CONSUMED |
| body 초과 | n/a | 413 PAYLOAD_TOO_LARGE | 413 BODY_TOO_LARGE |
| 저장소/내부 실패 | 500 TX/500 INTERNAL | 500 INTERNAL | 500 INTERNAL |
| 서버 설정 오류 | 시작 panic | 500 INTERNAL | 500 CONFIG |
| 소유권/스코프 거부 | 403 FORBIDDEN | 403 FORBIDDEN | 403 RESOURCE_FORBIDDEN |
| 레이트 리밋 | 429 bare 상태(전역 RPM 리미터; /auth/login은 5회 실패 잠금 추가) | Worker 플랫폼 / 미모델 | 어댑터 관심사, 미모델 |
문서화된 델타(의도적, 버그 아님):
VALIDATIONvsINVALID_PUBLIC_JWK: Worker는 나쁜 디바이스 JWK를VALIDATION에 폴드; SDK는 구분. 둘 다400.FORBIDDENvsCAPABILITY_INVALID: Worker는 capability 검증 실패에FORBIDDEN반환; SDK는CAPABILITY_INVALID사용. 둘 다403.403vs404NOT_FOUND: Worker는 알 수 없는 라이선스 키에 의도적으로403 NOT_FOUND응답(다른 활성화 거부와 같은 클래스); SDK는404사용. 웹 클라이언트는LicenseError.code의{code}를 그대로 보고status는 실제 HTTP 상태.PAYLOAD_TOO_LARGEvsBODY_TOO_LARGE: 둘 다413.- 알 수 없는
status→INVALID_STATUS(Worker/Axum) vsREVOKED(SDK): SDK는 인식 못하는 라이선스 상태를 폐기됨으로 취급(fail closed). - † Worker
403"동일 코드"는/activate에만 적용:/activate는license_gate판정 코드를 그대로 반환(license_security.rs:412-419, 예:403 REVOKED). execute/capability 가드 경로 (license_security.rs:259-279)에서INVALID_STATUS가 아닌 게이트 판정은403 FORBIDDEN으로 축소 — 구체적 코드는license_blocked보안 이벤트에만 남음.
3. 활성화 경로
| 표면 | 라우트 | 모델 |
|---|---|---|
Axum be/(레거시 레코드) | POST /api/v1/licenses/verify(200 body 판정), POST /api/v1/licenses/:id/activations, DELETE /api/v1/licenses/:id/activations/:device | activations_json 디바이스 레코드 — PoP 없음, entitlement 없음 |
Worker PoP(be-cloudflare) | POST /api/v1/licenses/activate → {entitlement, expires_in, features}; POST /api/v1/licenses/capability; POST /api/v1/licenses/execute; GET /api/v1/licenses/status | entitlement JWS + DPoP 증명 + license_bindings |
SDK 기본(@yetdapay/license-web) | POST /api/license/activate(client.js DEFAULT_ACTIVATE_PATH) | PoC/Nuxt 스타일 자체 호스팅 서버 |
| SDK worker 프리셋 | POST /api/v1/licenses/activate(createWorkerConfig() / WORKER_ACTIVATE_PATH) | 동일 wire 형식, Worker 라우트 |
요청 body는 양쪽 경로에서 동일: {key, product_id, public_jwk} → {entitlement, expires_in, features}. 라우트만 다름.
4. 버전 호환성
라이선스 SDK + MCP 패키지는 0.1.0; @yetdapay/cli는 wire 계약 v1에서 0.2.0; 토큰, 헤더, 엔벨로프에 버전 필드가 없으므로(known-limitations.md §3.2) 호환성은 "동일 계약 세대" — 협상되지 않음.
| 구성 요소 | 패키지/바이너리 | 버전 | 계약 |
|---|---|---|---|
| 브라우저 라이선스 SDK | @yetdapay/license-web | 0.1.0 | wire v1; createWorkerConfig()로 Worker 페어링 |
| 서버 라이선스 SDK | @yetdapay/license-server | 0.1.0 | wire v1; createWorkerConfig()로 Worker 클레임 정책 |
| Nuxt 어댑터 | @yetdapay/license-nuxt | 0.1.0 | wire v1(license-web/license-server 래핑) |
| CLI | @yetdapay/cli(yp 바이너리) | 0.1.0 | 로컬 protect 툴링; 아직 라이선스 wire 호출 없음 |
| 커머스 SDK | @yetdapay/sdk | 0.1.0 | 기부/피드백 REST(오늘 Axum 전용 엔드포인트) |
| 로컬 MCP | yetdapay-mcp 0.1.0 | 0.1.0 | stdio 위 JSON-RPC, MCP 2024-11-05, be/의 REST 클라이언트 |
| 원격 MCP 게이트웨이 | yetdapay-mcp-gateway 0.1.0 | 0.1.0 | Streamable HTTP 위 동일 도구 |
| 백엔드 | Axum be/ · be-cloudflare Worker | 리포 SHA 버전 | REST /api/v1/**; PoP entitlement wire v1(Worker 전용) |
5. 연기된 후속
- Worker entitlement의
aud—DEFERRED. Worker는aud를 발급하지도 검증하지도 않음; 추가하면 토큰 형식이 변경됨(wire v2 후보).known-limitations.md§3.8에 기록. - Worker entitlement의
tenant_id—DEFERRED. 동일한 wire 형식 영향. SDK는 이미 이를 발급/정규화(nullable). - 오류 코드 수렴(
INVALID_PUBLIC_JWK,CAPABILITY_INVALID,PAYLOAD_TOO_LARGE,INVALID_STATUS) — 위 문서화된 델타; 통일은 문서 수정이 아닌 wire v2 결정.