feat(core-shared): add rate-limit type primitives and manifest field
Adds IRateLimit, RateLimitBudget, RateLimitDecision to a new rate-limit sub-module; adds RateLimited<F> brand and isRateLimited predicate following the Captured/ConsentChecked pattern; extends UseCaseManifest with rateLimit?: readonly RateLimitBudget[] so features can declare rate-limit gates in their manifests. Exports new types from @repo/core-shared/rate-limit and @repo/core-shared/conformance. Blocks stories 02, 03, and 04. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-19T21:51:40.371Z",
|
"generatedAt": "2026-05-20T08:21:18.294Z",
|
||||||
"commit": "5151454",
|
"commit": "a633561",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 97.38,
|
"statements": 97.38,
|
||||||
"branches": 92.19,
|
"branches": 92.2,
|
||||||
"functions": 97.1,
|
"functions": 97.11,
|
||||||
"lines": 97.38,
|
"lines": 97.38,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 5801,
|
"lf": 5807,
|
||||||
"lh": 5649,
|
"lh": 5655,
|
||||||
"brf": 1165,
|
"brf": 1166,
|
||||||
"brh": 1074,
|
"brh": 1075,
|
||||||
"fnf": 345,
|
"fnf": 346,
|
||||||
"fnh": 335
|
"fnh": 336
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"byPackage": {
|
"byPackage": {
|
||||||
@@ -101,17 +101,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/core-shared": {
|
"@repo/core-shared": {
|
||||||
"statements": 98.03,
|
"statements": 98.04,
|
||||||
"branches": 95.89,
|
"branches": 95.9,
|
||||||
"functions": 92.08,
|
"functions": 92.16,
|
||||||
"lines": 98.03,
|
"lines": 98.04,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 1067,
|
"lf": 1073,
|
||||||
"lh": 1046,
|
"lh": 1052,
|
||||||
"brf": 316,
|
"brf": 317,
|
||||||
"brh": 303,
|
"brh": 304,
|
||||||
"fnf": 101,
|
"fnf": 102,
|
||||||
"fnh": 93
|
"fnh": 94
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/core-ui": {
|
"@repo/core-ui": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./audit": "./src/audit/index.ts",
|
"./audit": "./src/audit/index.ts",
|
||||||
|
"./rate-limit": "./src/rate-limit/index.ts",
|
||||||
"./conformance": "./src/conformance/index.ts",
|
"./conformance": "./src/conformance/index.ts",
|
||||||
"./conformance/coverage": "./src/conformance/coverage.ts",
|
"./conformance/coverage": "./src/conformance/coverage.ts",
|
||||||
"./di": "./src/di/index.ts",
|
"./di": "./src/di/index.ts",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isCaptured,
|
isCaptured,
|
||||||
isAudited,
|
isAudited,
|
||||||
isAnalyzed,
|
isAnalyzed,
|
||||||
|
isRateLimited,
|
||||||
} from "@/conformance/brand-runtime";
|
} from "@/conformance/brand-runtime";
|
||||||
|
|
||||||
describe("brand-runtime", () => {
|
describe("brand-runtime", () => {
|
||||||
@@ -23,6 +24,7 @@ describe("brand-runtime", () => {
|
|||||||
expect(isCaptured(fn)).toBe(false);
|
expect(isCaptured(fn)).toBe(false);
|
||||||
expect(isAudited(fn)).toBe(false);
|
expect(isAudited(fn)).toBe(false);
|
||||||
expect(isAnalyzed(fn)).toBe(false);
|
expect(isAnalyzed(fn)).toBe(false);
|
||||||
|
expect(isRateLimited(fn)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("predicates discriminate between brands", () => {
|
it("predicates discriminate between brands", () => {
|
||||||
@@ -45,6 +47,11 @@ describe("brand-runtime", () => {
|
|||||||
const analyzed = () => {};
|
const analyzed = () => {};
|
||||||
attachBrand(analyzed, "__analyzed");
|
attachBrand(analyzed, "__analyzed");
|
||||||
expect(isAnalyzed(analyzed)).toBe(true);
|
expect(isAnalyzed(analyzed)).toBe(true);
|
||||||
|
|
||||||
|
const rateLimited = () => {};
|
||||||
|
attachBrand(rateLimited, "__rateLimited");
|
||||||
|
expect(isRateLimited(rateLimited)).toBe(true);
|
||||||
|
expect(isInstrumented(rateLimited)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("composing brands stacks them on the same function", () => {
|
it("composing brands stacks them on the same function", () => {
|
||||||
@@ -53,10 +60,12 @@ describe("brand-runtime", () => {
|
|||||||
attachBrand(fn, "__captured");
|
attachBrand(fn, "__captured");
|
||||||
attachBrand(fn, "__audited");
|
attachBrand(fn, "__audited");
|
||||||
attachBrand(fn, "__analyzed");
|
attachBrand(fn, "__analyzed");
|
||||||
|
attachBrand(fn, "__rateLimited");
|
||||||
expect(isInstrumented(fn)).toBe(true);
|
expect(isInstrumented(fn)).toBe(true);
|
||||||
expect(isCaptured(fn)).toBe(true);
|
expect(isCaptured(fn)).toBe(true);
|
||||||
expect(isAudited(fn)).toBe(true);
|
expect(isAudited(fn)).toBe(true);
|
||||||
expect(isAnalyzed(fn)).toBe(true);
|
expect(isAnalyzed(fn)).toBe(true);
|
||||||
|
expect(isRateLimited(fn)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("attached brands are non-writable and non-configurable", () => {
|
it("attached brands are non-writable and non-configurable", () => {
|
||||||
|
|||||||
@@ -13,14 +13,15 @@
|
|||||||
* commitment, not a mutable flag.
|
* commitment, not a mutable flag.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Analyzed, ConsentChecked } from "./brands";
|
import type { Analyzed, ConsentChecked, RateLimited } from "./brands";
|
||||||
|
|
||||||
type Brand =
|
type Brand =
|
||||||
| "__instrumented"
|
| "__instrumented"
|
||||||
| "__captured"
|
| "__captured"
|
||||||
| "__audited"
|
| "__audited"
|
||||||
| "__analyzed"
|
| "__analyzed"
|
||||||
| "__consentChecked";
|
| "__consentChecked"
|
||||||
|
| "__rateLimited";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches the brand as a non-enumerable property on the given function.
|
* Attaches the brand as a non-enumerable property on the given function.
|
||||||
@@ -68,3 +69,9 @@ export function isConsentChecked<F extends object>(
|
|||||||
): fn is ConsentChecked<F> {
|
): fn is ConsentChecked<F> {
|
||||||
return hasBrand(fn, "__consentChecked");
|
return hasBrand(fn, "__consentChecked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isRateLimited<F extends object>(
|
||||||
|
fn: unknown,
|
||||||
|
): fn is RateLimited<F> {
|
||||||
|
return hasBrand(fn, "__rateLimited");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expectTypeOf } from "vitest";
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
import type { Instrumented, Captured } from "@/conformance/brands";
|
import type { Instrumented, Captured, RateLimited } from "@/conformance/brands";
|
||||||
|
|
||||||
describe("brand types", () => {
|
describe("brand types", () => {
|
||||||
it("Instrumented<F> is structurally F plus a phantom flag", () => {
|
it("Instrumented<F> is structurally F plus a phantom flag", () => {
|
||||||
@@ -16,6 +16,12 @@ describe("brand types", () => {
|
|||||||
expectTypeOf<Captured<Fn>["__captured"]>().toEqualTypeOf<true>();
|
expectTypeOf<Captured<Fn>["__captured"]>().toEqualTypeOf<true>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("RateLimited<F> is structurally F plus a phantom flag", () => {
|
||||||
|
type Fn = (n: number) => Promise<string>;
|
||||||
|
expectTypeOf<RateLimited<Fn>>().toBeCallableWith(1);
|
||||||
|
expectTypeOf<RateLimited<Fn>["__rateLimited"]>().toEqualTypeOf<true>();
|
||||||
|
});
|
||||||
|
|
||||||
it("brands compose without conflict", () => {
|
it("brands compose without conflict", () => {
|
||||||
type Fn = (n: number) => Promise<string>;
|
type Fn = (n: number) => Promise<string>;
|
||||||
type Both = Instrumented<Fn> & Captured<Fn>;
|
type Both = Instrumented<Fn> & Captured<Fn>;
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export type Instrumented<F> = F & { readonly __instrumented: true };
|
|||||||
export type Captured<F> = F & { readonly __captured: true };
|
export type Captured<F> = F & { readonly __captured: true };
|
||||||
export type Analyzed<F> = F & { readonly __analyzed: true };
|
export type Analyzed<F> = F & { readonly __analyzed: true };
|
||||||
export type ConsentChecked<F> = F & { readonly __consentChecked: true };
|
export type ConsentChecked<F> = F & { readonly __consentChecked: true };
|
||||||
|
export type RateLimited<F> = F & { readonly __rateLimited: true };
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { describe, it, expect, expectTypeOf } from "vitest";
|
|||||||
import {
|
import {
|
||||||
defineFeature,
|
defineFeature,
|
||||||
type FeatureManifest,
|
type FeatureManifest,
|
||||||
|
type UseCaseManifest,
|
||||||
} from "@/conformance/define-feature";
|
} from "@/conformance/define-feature";
|
||||||
|
import type { RateLimitBudget } from "@/rate-limit/rate-limit.interface";
|
||||||
import {
|
import {
|
||||||
DEFAULT_COVERAGE_BANDS,
|
DEFAULT_COVERAGE_BANDS,
|
||||||
DEFAULT_MUTATION_SCORE,
|
DEFAULT_MUTATION_SCORE,
|
||||||
@@ -58,6 +60,41 @@ describe("defineFeature", () => {
|
|||||||
expectTypeOf(manifest).toMatchTypeOf<FeatureManifest>();
|
expectTypeOf(manifest).toMatchTypeOf<FeatureManifest>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts an optional rateLimit array on a use case", () => {
|
||||||
|
const manifest = defineFeature({
|
||||||
|
name: "auth",
|
||||||
|
requiredCores: [],
|
||||||
|
useCases: {
|
||||||
|
signIn: {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
rateLimit: [{ name: "login", window: "1m", budget: 5 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realtimeChannels: [],
|
||||||
|
jobs: [],
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
expectTypeOf(manifest).toMatchTypeOf<FeatureManifest>();
|
||||||
|
expectTypeOf(manifest.useCases.signIn.rateLimit).toMatchTypeOf<
|
||||||
|
readonly RateLimitBudget[] | undefined
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("use case without rateLimit satisfies UseCaseManifest", () => {
|
||||||
|
const uc: UseCaseManifest = {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
};
|
||||||
|
expectTypeOf(uc.rateLimit).toEqualTypeOf<
|
||||||
|
readonly RateLimitBudget[] | undefined
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts an optional coverage section", () => {
|
it("accepts an optional coverage section", () => {
|
||||||
const manifest = defineFeature({
|
const manifest = defineFeature({
|
||||||
name: "auth",
|
name: "auth",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CoverageManifest } from "./coverage";
|
import type { CoverageManifest } from "./coverage";
|
||||||
|
import type { RateLimitBudget } from "../rate-limit/rate-limit.interface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-use-case manifest entry. Declares what the use case does at the contract
|
* Per-use-case manifest entry. Declares what the use case does at the contract
|
||||||
@@ -12,6 +13,7 @@ export type UseCaseManifest = {
|
|||||||
readonly publishes: readonly string[];
|
readonly publishes: readonly string[];
|
||||||
readonly consumes: readonly string[];
|
readonly consumes: readonly string[];
|
||||||
readonly analyticsEvents?: readonly string[];
|
readonly analyticsEvents?: readonly string[];
|
||||||
|
readonly rateLimit?: readonly RateLimitBudget[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type {
|
|||||||
Captured,
|
Captured,
|
||||||
Analyzed,
|
Analyzed,
|
||||||
ConsentChecked,
|
ConsentChecked,
|
||||||
|
RateLimited,
|
||||||
} from "./brands";
|
} from "./brands";
|
||||||
export type { FeatureManifest, UseCaseManifest } from "./define-feature";
|
export type { FeatureManifest, UseCaseManifest } from "./define-feature";
|
||||||
export { defineFeature } from "./define-feature";
|
export { defineFeature } from "./define-feature";
|
||||||
@@ -29,6 +30,7 @@ export {
|
|||||||
isAudited,
|
isAudited,
|
||||||
isAnalyzed,
|
isAnalyzed,
|
||||||
isConsentChecked,
|
isConsentChecked,
|
||||||
|
isRateLimited,
|
||||||
} from "./brand-runtime";
|
} from "./brand-runtime";
|
||||||
export { ConformanceError } from "./conformance-error";
|
export { ConformanceError } from "./conformance-error";
|
||||||
export { assertFeatureConformance } from "./assert-bindings";
|
export { assertFeatureConformance } from "./assert-bindings";
|
||||||
|
|||||||
5
packages/core-shared/src/rate-limit/index.ts
Normal file
5
packages/core-shared/src/rate-limit/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type {
|
||||||
|
IRateLimit,
|
||||||
|
RateLimitBudget,
|
||||||
|
RateLimitDecision,
|
||||||
|
} from "./rate-limit.interface";
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, it, expectTypeOf } from "vitest";
|
||||||
|
import type {
|
||||||
|
IRateLimit,
|
||||||
|
RateLimitBudget,
|
||||||
|
RateLimitDecision,
|
||||||
|
} from "@/rate-limit/rate-limit.interface";
|
||||||
|
|
||||||
|
describe("rate-limit types", () => {
|
||||||
|
it("RateLimitBudget has the expected shape", () => {
|
||||||
|
expectTypeOf<RateLimitBudget>().toMatchTypeOf<{
|
||||||
|
name: string;
|
||||||
|
window: string;
|
||||||
|
budget: number;
|
||||||
|
}>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("RateLimitDecision has the expected shape", () => {
|
||||||
|
expectTypeOf<RateLimitDecision>().toMatchTypeOf<{
|
||||||
|
allowed: boolean;
|
||||||
|
remaining: number;
|
||||||
|
resetAt: Date;
|
||||||
|
}>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("IRateLimit.consume returns Promise<RateLimitDecision>", () => {
|
||||||
|
expectTypeOf<
|
||||||
|
IRateLimit["consume"]
|
||||||
|
>().returns.resolves.toEqualTypeOf<RateLimitDecision>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("IRateLimit.consume accepts optional weight", () => {
|
||||||
|
type ConsumeParams = Parameters<IRateLimit["consume"]>;
|
||||||
|
expectTypeOf<ConsumeParams[0]>().toEqualTypeOf<string>();
|
||||||
|
expectTypeOf<ConsumeParams[1]>().toEqualTypeOf<string>();
|
||||||
|
expectTypeOf<ConsumeParams[2]>().toEqualTypeOf<number | undefined>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("IRateLimit.reset returns Promise<void>", () => {
|
||||||
|
expectTypeOf<IRateLimit["reset"]>().returns.resolves.toEqualTypeOf<void>();
|
||||||
|
});
|
||||||
|
});
|
||||||
20
packages/core-shared/src/rate-limit/rate-limit.interface.ts
Normal file
20
packages/core-shared/src/rate-limit/rate-limit.interface.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export type RateLimitBudget = {
|
||||||
|
name: string;
|
||||||
|
window: string;
|
||||||
|
budget: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RateLimitDecision = {
|
||||||
|
allowed: boolean;
|
||||||
|
remaining: number;
|
||||||
|
resetAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IRateLimit {
|
||||||
|
consume(
|
||||||
|
budgetName: string,
|
||||||
|
key: string,
|
||||||
|
weight?: number,
|
||||||
|
): Promise<RateLimitDecision>;
|
||||||
|
reset(budgetName: string, key: string): Promise<void>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user