API 계약 맵 — DevPay 제품 표면
읽기 안내
해당 커밋 시점의 API 경로 목록입니다. Worker와 Axum은 제공 범위가 다르므로 현재 코드와 대조하세요.
일반 사용법은 엔드포인트 안내부터 확인하고 정확한 권한·호환성이 필요할 때 이 표를 사용하세요.
상태: HEAD ad360cf(베이스 cb0270be 계보)에 존재하는 것의 인벤토리. 라우트 진실 공급원: be-cloudflare/src/lib.rs 디스패치 테이블. Axum be/는 레거시 서브셋을 미러(be/src/lib.rs 라우트) — license-security 엔드포인트는 Worker 전용(Axum 패리티 없음). 레거시 표면의 Proto DTO: contracts/proto/nougat/v1/nougat.proto.
1. be-cloudflare Worker 라우트 (/api/v1)
Auth 및 계정
| Method | Path | Auth | 노트 |
|---|
| POST | /auth/signup | public | usr_{12hex} + role='buyer'(auth_impl.rs:366) |
| POST | /auth/login | public | JWT iss/aud/sub/role/seller_id/tv/iat/exp |
| POST | /auth/logout | user | token_version 범프 |
| GET | /me | user | FE 가드가 사용하는 role + seller_id |
| PATCH | /me/password | user | PBKDF2, 토큰 무효화 |
| POST | /me/deactivate | user | status→inactive |
체크아웃 및 결제 (레거시 커머스 스파인)
| Method | Path | Auth | 노트 |
|---|
| POST | /checkout | public | 멱등(idempotency_key); mock 프로바이더 → 원자적 order/payment/subscription/license/ledger/audit/outbox 배치; toss → pending order + 위젯 페이로드(toss_impl::checkout_toss); live env + mock → 402 NOT_LIVE_PAYMENT |
| POST | /checkout/confirm | public | Toss confirm → finalize_paid(mock paid와 동일 부작용) |
| POST | /webhooks/toss | public | 서명 검증(TOSS webhook secret), 참조 검증, PG 상태 재조회 — body 절대 신뢰 안 함 |
| GET | /orders | seller/admin | 스코프 목록 |
| GET | /orders/:id | seller/admin | |
| POST | /orders/:id/refund | seller(owner) | refund_attempts 사전 기록 → PG 취소 → succeeded|failed|needs_review → ledger/license 보정 |
| GET | /orders/:id/refund-attempts | seller(owner) | 시도 감사 뷰 |
| GET | /payments/:id | seller/admin | |
| POST | /purchase/game-item | user | 게임 아이템 구매 + 이행(purchase_impl.rs auth) |
| POST | /fulfillments/:id/retry | seller | |
상품
| Method | Path | Auth | 노트 |
|---|
| GET | /products | seller/admin | 스코프 목록 — effective_role_sid + list_scope; 셀러는 자기 행, admin은 무스코프(lib.rs:319) |
| POST | /products | seller | product_create — prod_{12hex}, 유일 (seller_id, slug) → 409 CONFLICT(lib.rs:454, verify_impl.rs:172) |
| PATCH/PUT | /products/:id | seller(owner) | product_update — 소유권 검사 → 타인 행은 404 NOT_FOUND(lib.rs:458) |
| DELETE | /products/:id | seller(owner)/admin | ep::delete_product — scope(): None(admin)=모든 행, Some(sid)=자기 것만; 비소유 → 404(lib.rs:645, endpoints_impl.rs:919) |
License — 레거시 플레인
| Method | Path | Auth | 노트 |
|---|
| POST | /licenses | seller | 발급(lic_ + NG-… 키) |
| GET | /licenses | seller/admin | 스코프 목록 |
| PATCH | /licenses/:id | seller(owner) | status/expires_at |
| POST | /licenses/:id/activations | seller(owner) | 레거시 device_id 레코드(표시 전용) |
| DELETE | /licenses/:id/activations/:device | seller(owner) | |
| POST | /licenses/verify | public | 조회 전용 판정 {valid,code,status,product_id,product_name,expires_at,max_devices,activated_devices,device_registered,checked_at}; 코드 OK|NOT_FOUND|PRODUCT_MISMATCH|REVOKED|SUSPENDED|EXPIRED|INVALID_STATUS; 슬롯 소비 안 함 |
License — 보안 플레인 (PoP; LICENSE_SECURITY_ENABLED=true 정확히 일치로 게이트; 아니면 preflight 포함 404)
| Method | Path | 필요한 증명 | 노트 |
|---|
| POST | /licenses/activate | 없음(라이선스 키) | {key, product_id, public_jwk} → JWK on-curve 검증 → 게이트 검사 → 원자적 license_bindings 슬롯(UNIQUE(lic_id,jkt), count<max_devices) → {entitlement, expires_in:120, features} |
| POST | /licenses/capability | entitlement + DPoP | {feature} → {cap_id, feature, exp}(60초) |
| POST | /licenses/execute | entitlement + DPoP + capability | {feature,input,cap_id} → 속성 부여 + 원자적 소비 → 내장 premium_convert만 → {result} + license_usage 기록(최선 노력); 재사용 → 409 CAPABILITY_CONSUMED |
| GET | /licenses/status | entitlement + DPoP | {license:{product_id,product_name,status,expires_at,max_devices,bound_devices,activated_devices,features,entitlement_exp}} |
가드 순서(guard()): entitlement sig/iss/exp → 라이선스 행 재조회 (revoke/expire/product-mismatch) → DPoP sig/htm/htu/iat → jkt 일치 → jti 원자적 소비(license_nonces) → body 해시 h. 거부는 security_events를 씀(detail_json에 키/토큰/PII 없음).
Cron(5분, wrangler.toml crons): dispatch_outbox 항상; prune_expired(nonces+capabilities)는 플래그 활성 시만.
구독 / 정산
| Method | Path | Auth | 노트 |
|---|
| GET | /subscriptions | seller/admin | |
| POST | /subscriptions/:id/event | seller(owner) | {event: renew|fail|expire|cancel} — renew는 paid order+ledger 발행; live 결제에서 차단(402) |
| GET | /settlements | seller/admin | |
| POST | /settlements | seller | 발생 실행: seller_settings 요율에서 gross/pg_fee/platform_fee/net; set_, ST-YYYY-NNNN, status pending |
셀러 admin / 설정 / 검증
| Method | Path | Auth | 노트 |
|---|
| POST | /seller/onboarding | user | seller_{…} + mem_ + sset_ + ver_ 생성 |
| GET/PATCH | /seller | seller | 프로필, 지급 필드 |
| GET/PATCH | /settings | seller | api_key, webhook, pg_fee_rate, platform_fee_rate, default_max_devices, store_published |
| GET | /verification | seller | |
| POST | /verification/apply | seller | ver_{…} 행 |
| POST | /admin/verification/review | admin | |
플랫폼 운영자 (/admin)
| Method | Path | Auth | 노트 |
|---|
| GET | /admin/users · PATCH /admin/users/:id/status | admin | |
| GET | /admin/overview · /admin/sellers · /admin/verifications · /admin/audit | admin | |
| GET | /metrics | admin | |
| GET | /ledger/verify · /audit/verify | admin | 무결성 체인 재계산 |
프로젝트 / 게임 아이템 커머스 (레거시)
| Method | Path | Auth |
|---|
| GET/POST | /projects · GET/PATCH /projects/:id | seller/admin |
| GET | /projects/:id/items · /players · /fulfillments | seller/admin |
| POST/PATCH | /items · /items/:id | seller |
| GET/POST · PATCH/PUT · DELETE | /products · /products/:id | seller(owner)/admin — 위 "상품" 섹션 참조 |
| GET/POST/PATCH | /feedback-widgets + /:id | seller |
| GET/POST/PATCH | /feedbacks + /:id | 혼합(공개 위젯 post, 셀러 patch) |
| POST | /feedback-payments | public(유료 피드백) |
| GET | /widget-config/:id | public |
| GET | /seller/events | seller |
| GET | /dashboard/stats | seller/admin |
공개 스토어
| Method | Path | 노트 |
|---|
| GET | /public/stores/:slug · /public/stores/:slug/products · /public/stores/:slug/products/:id | store_published 게이트 |
| GET | /public/orders/:order_no?email= | order_no + email로 바이어 영수증 조회(public_impl.rs) |
Health 및 메트릭
GET /api/v1/health → {ok, request_id}(ok = D1 바인딩 존재, 공개). GET /api/v1/metrics → {ok, request_id, metrics}(admin).
2. sdk/license wire 계약 (BYOS 플레인)
세 패키지 — @yetdapay/license-web(브라우저), @yetdapay/license-server (호스트), @yetdapay/license-nuxt(어댑터). 출처: sdk/license/README.md 계약 표; 서버 엔트리 sdk/license/server/src/server.mjs.
| 요소 | 값 |
|---|
| 활성화 | POST {activatePath}(SDK 기본 /api/license/activate — Worker /api/v1/licenses/activate와 다름; activatePath 전달) body {key, product_id, public_jwk} → {entitlement, expires_in, features} |
| Auth 헤더 | Authorization: Entitlement <entitlement-jws> |
| 증명 헤더 | DPoP: <proof-jws> 페이로드 {htu, htm, iat, jti, h, jwk} |
htu | same-origin pathname + query(전체 URI 아님) |
h | base64url(sha256(raw request body)) |
jkt | sha256(RFC 7638 JWK thumbprint) — DevPay 고유 이중 해시; entitlement는 jkt_bound + cnf.jkt를 가짐 |
| Entitlement 클레임 | iss, aud?, lic_id, product_id, tenant_id?, features, jkt_bound, cnf, iat, exp — Worker는 iss=devpay-lic 발급, aud/tenant_id 없음; SDK 기본 iss=devpay |
| TTL / 윈도우 | entitlement 120초 · 증명 iat ±60초 · capability 60초 · nonce TTL 600초 |
| 오류 | {code, message} — 내부 이유 절대 노출 안 함; 저장소 실패 → INTERNAL(fail closed) |
| 저장소 계약 | LicenseStore: getProduct/findLicenseByKey/getLicense/bindDevice/countBindings/setLicenseStatus/consumeNonce/createCapability/getCapability/consumeCapability(+선택적 pruneNonces) — sqlite + memory 어댑터 출시됨 |
| 서버 API | createLicenseServer({store, keys, issuer, audience, entitlementTtlSec, expectedTenantId, resourceOwnership}) → activate() / authorize() / issueCapability(); createNuxtLicense → defineLicenseEventHandler({feature}, handler) |
| 클라이언트 API | createLicenseClient({productId, activatePath?, …}) → activate({licenseKey}), signedFetch(url, init)(401에서 entitlement 한 번 자동 갱신) |
상호운용 델타(문서화된 블로커, p5-sdk-contract-audit.md): SDK 기본값(/api/license/activate, iss=devpay, 선택적 aud)은 Worker 값(/api/v1/licenses/activate, iss=devpay-lic, aud 없음)과 일치하지 않습니다. 상호 상호운용은 명시적 path/issuer 설정으로만 증명됩니다. Wire 형식은 DevPay 고유 버전 계약이며 RFC 9449 DPoP가 아닙니다.
3. Auto-protection CLI 표면 (루프백 툴링, HTTP 아님)
| 엔트리 | 호출 | 계약 |
|---|
| 분석기 | node analyzer/run.mjs <target.{mjs,js,wasm}> [--driver x.mjs] [--out dir] [--no-dynamic] [--stdout] / --all-samples | → *.analysis.json(8클래스 판정, L0–L4, 신뢰도) |
| Spec 검증 | node spec/validate.mjs <spec.json> | exit 0/≠0; 폐쇄 불변식 레지스트리 + REQUIRES_APPROVAL 마커 강제 |
| 요구사항에서 spec | node spec/from-requirements.mjs <requirements.json> [--overrides o.json] [--out spec.json] | Track-B 초안 |
| 분석에서 spec | spec/from-analysis.mjs | Track-A 초안 |
| 플래너 | planner/plan.mjs(track-a/b를 통해) | → ProtectionPlan(plan_version:1, spec_sha256, 에셋당 AUTO|AGENT|MANUAL_REVIEW, blocking_unknowns) |
| Track A | node track-a/run.mjs --target m.mjs [--driver d.mjs] --spec s.json [--out dir] · --draft [--overrides] · --apply-auto [--vectors v.json] [--partition-auth dev-open|yp-license] [--port N] [--license-seed f] | exit 0 ok / 2 input·invalid·refused / 3 apply 실패; sha256 기록 아티팩트 + track-a-report.md |
| Track B | node track-b/run.mjs --requirements reqs.json --out dir [--approved-spec s.json] [--analysis a.json] [--overrides o.json] [--now ISO] | requirements→draft→plan handoff 번들 |
| 파티셔너 | node partitioner/run.mjs <target.mjs> [--driver d.mjs] [--out dir] [--functions a,b] [--port N] [--auth dev-open|yp-license] [--allow-dev-open] [--feature NAME] [--vectors v.vectors.json] [--no-serve] | 거부 우선; runtime/을 import하는 생성 서버 + 파티션된 클라이언트 + 매니페스트 출력 |
| 증거 | evidence/record.mjs · evidence/validate.mjs · evidence/cross-track.mjs | 레코드 스키마 v1; 클레임 등급 게이트; 크로스 트랙 리포트 |
| 레드팀 | red-team/unified-attacks.mjs(+ 실험별 배터리) | → out/unified-attack-*, 레그별 트랜스크립트 |
생성된 파티션 서버는 루프백에서 auth 모드 dev-open(인증 없음; 옵트인
- 루프백 전용) 또는
yp-license(위 SDK entitlement 게이트)로 HTTP를 말합니다 — 파티션된 함수당 엔드포인트 등록, nonce 게이트 증명 카운트와 dev-open 전용 /api/usage 추가.
4. 계약의 일부가 아닌 것 (아직)
- 보호 spec/plan/evidence API 없음 — CLI/파일 아티팩트만.
- 셀러나 BYOS 호스트의 사용량 읽기나 미터링 보고 엔드포인트 없음.
- 셀러 빌링 / 플랫폼 수수료 수집 엔드포인트 없음.
activate/capability/execute/status의 Axum 라우트 없음 — Worker 전용.- FE 클라이언트(
fe/lib/api/client.ts)는 보안 플레인 라우트를 호출하지 않음 — 계약은 서버 측에만 존재.