제품 계약 — 개념 레지스트리
읽기 안내
설계 레퍼런스입니다. Seller·License·Entitlement·PaymentEvent 등의 식별자, 책임, 현재 상태와 제안 모델을 정리합니다.
처음이라면 핵심 개념을 먼저 보고, 데이터 모델 계약을 확인할 때 이 문서를 사용하세요.
상태: PROPOSAL, 현재 구현에 근거. "Identity"는 코드에서 발견된 실제 ID/키 형식; "Owner"는 레코드 진실에 책임 있는 당사자; "States"는 코드가 오늘 실제 쓰는 값; "Permission scope"는 현재 인가 하에서 읽고 쓸 수 있는 자. 설계로만 존재하는 개념(예: 일급 Buyer 엔티티)은 만들어내지 않고 그렇게 표시됩니다.
1. 레지스트리
Seller
- Identity:
seller테이블id=seller_{12hex}(auth_impl.rs:460); 레거시 시드 값seller_nougat. Slug:seller.slug→ 온보딩에서s-{…}; 공개 스토어 경로/store/{slug}. - Owner: DevPay(아이덴티티) + 셀러 본인(프로필/지급 필드).
- States: 오늘 셀러 수준 상태 기계 없음 —
seller행에status컬럼 없음. 검증 상태는 별도로(verification.status, 생성 시NOT_REGISTERED등). - Permission scope:
users.role='seller'+seller_memberships행 (role='owner'),seller_scope_db로 스코프 — 모든 요청의 DB 재검사. 셀러는 자기 products/orders/licenses/subscriptions/settlements만 봄.admin은 모두 봄.
Buyer
- Identity: 두 개의 부분 아이덴티티, 통합 엔티티 없음. (a)
users.id = usr_{12hex},role='buyer'(signup 기본,auth_impl.rs:366); (b)orders.buyer_email/licenses.buyer_email— 체크아웃은 계정 없이 email 문자열만 필요. - Owner:
users행은 DevPay; email 아이덴티티는 플랫폼 자격 증명이 아닌 셀러 가시 데이터. - States:
users.status=active|inactive(deactivate). email 전용 바이어는 상태 없음. - Permission scope: 바이어는 공개/체크아웃 엔드포인트와 자기
/me리소스 호출 가능; 바이어 대시보드 없음. 바이어 email은 소유권 증명이 아님(charter: 미검증 email은 구매를 주장하면 안 됨).
Product
- Identity:
products.id = prod_{12hex}(verify_impl.rsproduct_create); 생성/업데이트에서(seller_id, slug)유일 강제. - Owner: 소유 셀러; DevPay가 저장.
- States:
status—active| 비활성(체크아웃은PRODUCT_INACTIVE거부);type∈one_time|subscription|free|donation(FEProductType,fe/types/index.ts:25). - Permission scope:
/public/stores/…로 공개 읽기(발행된 셀러만); 셀러는 자기 상품 쓰기;features_json/plans_json/license_json/entitlements_json은 상품의 정책 페이로드 —features_json이 entitlement 네임스페이스의 유일한 진실 공급원.
License
- Identity:
licenses.id = lic_{12hex}(mock 경로,mk("lic")) 또는 결정론적lic_{ord_id}(Toss finalize,toss_impl.rs:321—INSERT OR IGNORE가 confirm/webhook 재시도를 멱등으로).licenses.key는 두 관찰된 형식: (a) 구매 발급(mock 체크아웃checkout_impl.rs:165, Tosstoss_impl.rs:323):NG-{8hex}-{8hex}-{8hex}-{8hex}— 128비트; (b)POST /licenses셀러 발급(endpoints_impl.rs:554-557, Axum 패리티):NG-{4hex}-{4hex}— 32비트만. - KNOWN_LIMITATION: 32비트 셀러 발급 키는 공개, 무인증, 무제한
POST /licenses/verify를 통해 무차별 가능 — ~4.3 × 10⁹ 키 공간은 API 요청 속도로 열거 가능. 셀러 발급 키를 128비트 형식으로 마이그레이션 및/또는verify레이트 리밋 권고(현재 둘 다 없음). - Owner: DevPay가 발급;
product_id(→ 셀러)와buyer_email에 귀속; 선택적order_id/subscription_id역참조. - States:
active→suspended(결제 실패),revoked(취소/환불),expired(기간 종료 /expires_at경과);verify_status_error는REVOKED|SUSPENDED|EXPIRED|INVALID_STATUS로 매핑(verify_impl.rs).max_devices(기본 3) 슬롯은license_bindings원자적 insert로 강제. - Permission scope: 셀러는 자기 라이선스 CRUD(
issue/patch/ activations); 공개licenses/verify는 조회 전용(슬롯 쓰기 없음); PoPactivate는 키 보유자 + 상품 바운드. - 두 디바이스 모델 공존: 레거시
activations_json(표시/레코드 전용 — 인가 입력 아님)과license_bindings(jkt PoP 슬롯 — 인가).
Entitlement
- Identity: 컴팩트 ES256 JWS; 클레임
iss(Workerdevpay-lic/ SDK 기본yp),lic_id,product_id,features[],jkt_bound,cnf.jkt,iat,exp— TTL 120초(license-crypto/lib.rs). SDK 변형은 선택적aud,tenant_id추가. - Owner: DevPay(Worker
iss=devpay-lic) 또는 셀러의 license-server (BYOS,iss설정 가능). 서명된 토큰은 무상태; 모든 사용에서 라이브 라이선스 상태 재조회 — 폐기가 토큰 유효성을 이김. - States: 내부 없음(exp 바운드); 유효 상태는 파생: valid | expired | revoked-by-license-status.
- Permission scope:
Authorization: Entitlement <jws>의 전송 자격 증명;products.features_json너머를 부여하지 않음.
Feature
- Identity:
products.features_json의 문자열(예:premium_convert=license-crypto/premium.rs의FEATURE_PREMIUM_CONVERT). - Owner: 셀러(상품 정의); DevPay가 멤버십 강제.
- States: 없음(엔티티가 아닌 네임스페이스 항목).
- Permission scope: entitlement
features[]⊂ 상품 기능; 알 수 없음/비어있음 →403 FORBIDDEN(has_feature,effective_featuresfail-closed). 오늘 DevPay 호스티드/licenses/execute에서premium_convert만 실행 가능 — 다른 기능 →400 unknown feature.
ProtectedOperation
- Identity:
(product_id, feature)쌍 — entitlement+capability로 게이트된 서버 측 액션. DevPay 호스티드:{feature, input, cap_id}로POST /licenses/execute. BYOS:license.server.authorize/issueCapability또는requireLicense뒤의 모든 셀러 라우트. - Owner: 호스티드
execute는 DevPay(내장premium_convert만); BYOS 연산은 셀러. - States: 호출당, 영속 객체 없음 — 성공은
UsageRecord출력. - Permission scope: entitlement + 새 DPoP 증명 +
lic_id+product_id +feature+jkt에 바인딩된 미소비 capability.
ExecutionCapability
- Identity:
execution_capabilities.cap_id = cap_{16hex}(new_id("cap")). - Owner: DevPay(Worker 발급) 또는 셀러 저장소(BYOS
createCapability). - States:
consumed0→1만;exp로 만료(TTL 60초,CAPABILITY_TTL_SEC); cronprune_expired로 정리. - Permission scope: 바인딩된 튜플
cap_id+lic_id+product_id+feature +jkt; 소비는 하나의 원자적UPDATE … WHERE consumed=0— 두 번째 사용 →409 CAPABILITY_CONSUMED+security_events(capability_reuse).
UsageRecord
- Identity:
license_usage.id = use_{16hex}; 거부/리플레이/슬롯 오버플로용security_events.id = sec_{16hex}도. - Owner: DevPay(호스티드
execute/guard가 씀). - States: 추가 전용 사실 행(
lic_id,feature,at,meta_json—cap_id,product_id포함). - Permission scope: execute 후 서버 측 최선 노력 기록(비트랜잭션 — trust-model §3 사용량 제한 참조); 오늘 읽기 엔드포인트 없음 → 셀러/바이어 가시성 NOT_IMPLEMENTED.
PaymentEvent
- Identity:
payments.id = pay_{12hex};payments.payment_key= PGpaymentKey(toss). 돈 효과는transaction_ledger(led_{12hex},event_type∈payment_captured|payment_refunded|currency_grant| currency_revoke|license_issued|settlement)와outbox(out_{12hex}, 토픽 예:order.checkout)에도 기록. - Owner: DevPay 원장이 권위; PG 상태는
checkout/confirm+webhooks/toss로 대사(서명 검증, 참조 검증, 재조회 — 웹훅 body를 절대 신뢰하지 않음). - States:
payments.status=pending(Toss intent,toss_impl.rs:82) |paid(mock 체크아웃 / finalized Toss) |captured(feedback payments,endpoints_impl.rs:865) |refunded;orders.status∈paid|failed|canceled|refunded|pending| confirm_pending|partially_refunded. - Permission scope: 셀러는 자기 order/payment 읽기; 공개 주문 상태는
/public/orders/:id.provider∈mock|toss— mock은PAYMENT_LIVE_ENABLED=true에서 차단.
RefundEvent
- Identity:
refund_attempts.id = ra_{12hex}(mk_id("ra")). - Owner: DevPay 레코드; 소유 셀러가 시작(
POST /orders/:id/refund); PG가 실제 취소 실행(/v1/payments/{key}/cancel). - States:
attempted→succeeded|failed|needs_review(timeout/5xx — 불확실, 해결 필요;order.refunded_krw는succeeded만 합산).GET /orders/:id/refund-attempts가 감사 뷰. - Permission scope: 셀러 전용, 소유권 스코프; 부분 환불은 남은
amount_krw로 상한; 전액 환불은 라이선스 폐기 + 이행 보정을 같은 배치에서.
Subscription
- Identity:
subscriptions.id = sub_{12hex}(mock 체크아웃) 또는sub_{ord_id}(toss 경로 —toss_impl.rs:320,lic_{ord_id}와 함께 결정론적);license_id,order_ids_json추적. - Owner: DevPay 레코드; 셀러는
POST /subscriptions/:id/event {event: renew|fail|expire|cancel}로 관리. - States:
trialing(트라이얼 플랜) |active|past_due|expired|canceled. 전이: renew→active(+새 paid order+ledger); fail→past_due(+license suspended); expire→expired(+license expired); cancel→canceled(+license revoked).canceled/expired는 renew에 대해 종단(409 SUB_ENDED). - Permission scope: 셀러 이벤트는 자기 상품으로 스코프;
renew는 비-live 결제 env 필요(live에서402 NOT_LIVE_PAYMENT).
Tenant
- Identity: 셀러 경계 —
projects/products/orders의seller_id컬럼 +seller_memberships; SDK에는tenantId/tenant_id클레임 개념 존재(server.mjsexpectedTenantId, entitlementtenant_id). - Owner: DevPay(
seller_scope_db를 통한 멤버십/스코프 강제). - States: 멤버십 행(
mem_{10hex},role='owner'). - Permission scope: 셀러 토큰 →
Some(seller_id)스코프 → WHERE 절; admin →None(무스코프). Worker entitlement는 tenant_id 클레임 없음 — 호스티드 경로의 테넌시는lic_id+product_id+jkt바인딩.
ProtectionSpecification
- Identity: JSON 문서,
version:1(spec/protection-spec.schema.json); 플랜 내부spec_sha256으로 다운스트림 바인딩.project.id가 주체를 명명. - Owner: 초안하는 셀러/에이전트; 승인은 도구 외부의 인간/프로세스 행위(서명 필드 없음 — 의도적).
- States:
draft→approved→implemented→verified. 초안은 리터럴REQUIRES_APPROVAL마커를 가질 수 있음; 마커 남은 spec의--apply-auto는 거부(exit 2). - Permission scope: 폐쇄 불변식 레지스트리(13 ID, 모두
requires_server) — 미등록 ID는 spec을 무효로;auto_partition: "always"는 거부 화면을 오버라이드 불가.
ProtectionEvidence
- Identity: 증거 레코드
schema_version:1(evidence/schema/evidence-schema.json) —(requirement_id, asset_id)당 하나; 아티팩트 sha256 고정;commit_sha+worktree_dirty기록. - Owner: 독립 검증 주체(QA/Security 레인) — 절대 구현 당사자 아님.
- States: 불변식당 판정
VERIFIED_WITHIN_SCOPE | FAILED | PARTIALLY_VERIFIED | NOT_VERIFIED | NOT_APPLICABLE(폐쇄 열거,evidence-schema.json:200); 레그 수준PASS | FAIL | ATTACK_BLOCKED | ATTACK_REPRODUCED | LIMITATION_CONFIRMED | NOT_VERIFIED | NOT_APPLICABLE. - Permission scope: 추가 전용 증거; 검증기는 클레임 게이트 강제 (빈 실행 ≠ 증거; 모든 주장 불변식은 판정 필요; 절대적 클레임은 claims-scan으로 거부).
2. 교차 아이덴티티 형식 (관찰됨)
ord_{12hex} order · pay_{12hex} payment · lic_{12hex}/lic_{ord_id} license · sub_{12hex}/sub_{ord_id} subscription · set_{12hex} settlement(settlement_no = ST-YYYY-NNNN) · ord_{…}+order_no = NG-YYYYMMDD-NNNN(카운터 원자적) · led/aud/out/act/gi/idem_{12hex} · cap/use/sec_{16hex} · ra_{12hex} refund attempt · usr_{12hex} user · seller_{12hex} seller · proj_{12hex} project · plr_{12hex} player · ff_{12hex} fulfillment · mem_{10hex} membership · sset_{10hex} settings · ver_{10hex}/ver_{seller_id} verification · lease_{8hex} outbox lease-owner 토큰(엔티티 아님 — 클레임 마커, checkout_impl.rs:285).
3. 모든 재설계에서 살아남아야 하는 불변식
products.features_json이 유일한 entitlement 네임스페이스.- 라이선스 상태는 모든 인가에서 재독 — 토큰은 절대 폐기를 무시 안 함.
- 모든 소비 프리미티브는 단일 원자적 문장(슬롯, jti, capability, 카운터, 부분 환불 상한).
- 누락 행 ≠ 권한 — 모든 조회 실패는 거부(fail closed).
device_id/레거시activations_json은 절대 인가 입력이 아님;license_bindings(jkt)가 인가 입력.