Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/entitlement-guard-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@croco/entitlements-core": patch
"@croco/openapi-spec": patch
"@croco/protocols-core": patch
---

Entitlement guard requirements are now emitted as route contract metadata and OpenAPI extensions, with explicit guard status and evidence.
4 changes: 3 additions & 1 deletion examples/saas-billing-golden-path/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const workspacePackages = [
"diagnostics-core",
"events-core",
"events-inmemory",
"framework-config",
"framework-context",
"framework-logger",
"health-core",
Expand Down Expand Up @@ -42,6 +43,7 @@ export default defineConfig({
},
test: {
environment: "node",
include: ["src/tests/**/*.spec.ts"],
include: ["src/**/*.test.ts", "src/**/*.spec.ts"],
exclude: ["**/node_modules/**", "**/dist/**"],
},
});
3 changes: 3 additions & 0 deletions packages/admin-react/src/tests/AdminPanel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe("BillingEntitlementAdminPanel", () => {
{
featureKey: "reports",
granted: true,
status: "allowed",
planId: "pro",
quota: 100,
remaining: 80,
Expand Down Expand Up @@ -120,6 +121,7 @@ describe("BillingEntitlementAdminPanel", () => {
exceeded: true,
featureKey: "api_calls",
granted: false,
status: "denied",
overagePolicy: "BLOCK",
quota: 100,
reason: "quota_exceeded",
Expand Down Expand Up @@ -149,6 +151,7 @@ describe("BillingEntitlementAdminPanel", () => {
featureKey: "advanced_exports",
granted: false,
reason: "entitlement_not_found",
status: "denied",
type: "boolean",
};

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/tests/contractsCheck.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function createGraph(diagnostics: ContractDiagnostic[] = []): ContractGraph {
path: "/users",
controllerPath: "/users",
access: { guards: [], roles: [] },
entitlements: [],
params: [],
inputSchema: null,
inputSchemas: { body: null, path: null, query: null, headers: null },
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/tests/contractsDiff.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ function createGraph(
path: methodName === "createUser" ? "/users" : path,
controllerPath: "/users",
access: { guards: [], roles: [] },
entitlements: [],
params: [],
inputSchema: null,
inputSchemas: { body: null, path: null, query: null, headers: null },
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ const currentDir = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
resolve: {
alias: {
"@croco/execution-core": resolve(currentDir, "../execution-core/src/index.ts"),
"@croco/migration-runner": resolve(currentDir, "../migration-runner/src/index.ts"),
"@croco/openapi-spec": resolve(currentDir, "../openapi-spec/src/index.ts"),
"@croco/problems-core": resolve(currentDir, "../problems-core/src/index.ts"),
"@croco/protocols-core": resolve(currentDir, "../protocols-core/src/index.ts"),
"@croco/rpc-codegen": resolve(currentDir, "../rpc-codegen/src/index.ts"),
},
},
test: {
Expand Down
33 changes: 30 additions & 3 deletions packages/entitlements-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ const manager = new EntitlementManager(
const result = await manager.check("tenant-1", "api_calls");
```

### 라우트와 서비스 경계 강제

`@RequireEntitlement`는 클래스나 메서드에 필요한 기능 키를 선언합니다. `EntitlementGuard`는 핸들러 실행 전에 tenant, user, route, resource를 명시적인 guard 입력으로 만들어 `EntitlementManager.check()`를 호출하고, 실패 시 표준 Problem을 throw합니다.

```ts
import { EntitlementGuard, RequireEntitlement } from "@croco/entitlements-core";

class ReportsController {
@RequireEntitlement({
feature: "reports.export",
resource: { type: "report", idParam: "reportId" },
})
exportReport() {
return "ok";
}
}
```

resource id는 `resource.id`로 고정하거나 `resource.idParam`을 통해 request params에서 가져올 수 있습니다. guard는 성공과 실패 모두 `entitlement.guard.allowed` / `entitlement.guard.denied` telemetry event를 남기며, `EntitlementAuditSink`를 컨테이너에 등록하면 동일한 evidence를 audit sink로 받을 수 있습니다.

## API 레퍼런스

### 핵심 클래스
Expand All @@ -48,22 +68,29 @@ const result = await manager.check("tenant-1", "api_calls");

### 데코레이터와 인터페이스

- `@RequireEntitlement`, 엔드포인트에 필요한 기능 키를 선언합니다.
- `@RequireEntitlement`, 클래스나 메서드에 필요한 기능 키와 resource 요구사항을 선언합니다.
- `SubscriptionProvider`, `PlanEntitlementRegistry`, `EntitlementQuotaChecker`, `EntitlementMeterLookup`, `EntitlementEventPublisher`
- `EntitlementAuditSink`, guard 허용/거부 evidence를 기록하는 audit sink입니다.

### 주요 타입

- `EntitlementRule`, `EntitlementCheckResult`, `EntitlementQuotaStatus`
- `EntitlementType`, `OveragePolicy`, `PlanEntitlements`
- `EntitlementCheckStatus`, `EntitlementType`, `OveragePolicy`, `PlanEntitlements`
- `EntitlementRequirement`, `EntitlementResourceRequirement`, `EntitlementGuardInput`
- `UsageHistoryEntry`, `UsageHistoryPeriod`

### 이벤트와 문제 타입

- 이벤트: `EntitlementDeniedEvent`, `EntitlementQuotaExceededEvent`, `EntitlementOverageAllowedEvent`
- 문제 타입: `EntitlementDeniedProblem`, `EntitlementNotFoundProblem`
- 문제 타입: `EntitlementDeniedProblem`, `EntitlementMissingPlanProblem`, `EntitlementInactiveSubscriptionProblem`, `EntitlementQuotaExceededProblem`, `EntitlementProviderUnavailableProblem`, `EntitlementNotFoundProblem`

## Contract artifacts

`@RequireEntitlement` metadata는 `ENTITLEMENT_REQUIREMENTS_KEY`로 저장되며 `@croco/protocols-core`의 contract graph snapshot에 포함됩니다. `@croco/openapi-spec`는 선언된 entitlement 요구사항을 operation-level `x-croco-entitlements` extension으로 내보냅니다. 이 필드는 OpenAPI/RPC consumer coverage에서 drift gate로 검사됩니다.

## 구현 포인트

- `BLOCK`, `WARN`, `ALLOW_WITH_OVERAGE` 세 가지 overage 정책을 지원합니다.
- `EntitlementCheckResult.status`는 `allowed`, `denied`, `soft-limit`, `overage-allowed`, `unknown` 상태를 사용해 guard/audit/telemetry evidence를 정규화합니다.
- `meterId`를 지정하면 metering-core의 실제 사용량과 quota를 연결할 수 있습니다.
- subscription, billing, membership 같은 패키지와 조합해 플랜 제한을 중앙에서 관리할 수 있습니다.
1 change: 1 addition & 0 deletions packages/entitlements-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@croco/framework-context": "workspace:*",
"@croco/metering-core": "workspace:*",
"@croco/problems-core": "workspace:*",
"@croco/telemetry-api": "workspace:*",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
Expand Down
28 changes: 28 additions & 0 deletions packages/entitlements-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,33 @@
*/
export { RequireEntitlement } from "./libs/decorators/RequireEntitlement";

/**
* route/service 경계에서 공유하는 entitlement requirement metadata contract입니다.
*/
export {
appendEntitlementRequirement,
defineEntitlementRequirement,
ENTITLEMENT_REQUIRED_KEY,
ENTITLEMENT_REQUIREMENTS_KEY,
getEntitlementRequirements,
} from "./libs/EntitlementRequirement";
export type {
EntitlementRequirement,
EntitlementRequirementMetadata,
EntitlementResourceRequirement,
} from "./libs/EntitlementRequirement";

/**
* 라우트 실행 전에 entitlement를 검사하는 가드입니다.
*/
export { EntitlementGuard } from "./libs/EntitlementGuard";
export type {
EntitlementGuardInput,
EntitlementGuardResource,
EntitlementGuardRoute,
EntitlementGuardSubject,
RouteExecutionContext,
} from "./libs/EntitlementGuard";

/**
* 플랜 규칙과 quota를 조합해 entitlement 결과를 계산하는 핵심 서비스입니다.
Expand Down Expand Up @@ -43,7 +66,12 @@ export * from "./libs/interfaces";
*/
export {
EntitlementDeniedProblem,
EntitlementInactiveSubscriptionProblem,
EntitlementMissingPlanProblem,
EntitlementNotFoundProblem,
EntitlementProviderUnavailableProblem,
EntitlementQuotaExceededProblem,
EntitlementRequirementProblem,
} from "./libs/problems/EntitlementProblems";

/**
Expand Down
Loading
Loading