feat(core-shared): add withRateLimit wrapper and conformance enforcement
- Add withRateLimit(rateLimit, fn) in rate-limit/with-rate-limit.ts, attaching the RateLimited brand at DI bind time - Extend wireUseCase to accept optional rateLimit?: IRateLimit and compose withRateLimit innermost (before analytics/audit); propagate __rateLimited through analytics + audit inline wrappers - Extend withSpan and withCapture PROPAGATED_BRANDS to include __rateLimited so the outermost binding carries the brand - Extend assertFeatureConformance to require __rateLimited brand when manifest.useCases[name].rateLimit.length > 0; refactored into helper functions to stay within complexity thresholds - Add rateLimit?: IRateLimit to BindContext; default to NoopRateLimit in web-next bindAllProduction and bindAllDevSeed aggregators - Unit tests for withRateLimit brand attachment, factory passthrough, and composition; synthetic fixture tests for conformance errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
PayloadJobQueue,
|
PayloadJobQueue,
|
||||||
type IJobQueue,
|
type IJobQueue,
|
||||||
} from "@repo/core-shared/jobs";
|
} from "@repo/core-shared/jobs";
|
||||||
|
import { NoopRateLimit } from "@repo/core-shared/rate-limit";
|
||||||
import { bindProductionBlog } from "@repo/blog/di/bind-production";
|
import { bindProductionBlog } from "@repo/blog/di/bind-production";
|
||||||
import { bindProductionAuth } from "@repo/auth/di/bind-production";
|
import { bindProductionAuth } from "@repo/auth/di/bind-production";
|
||||||
import { bindProductionMarketingPages } from "@repo/marketing-pages/di/bind-production";
|
import { bindProductionMarketingPages } from "@repo/marketing-pages/di/bind-production";
|
||||||
@@ -97,6 +98,7 @@ export async function bindAllProduction(): Promise<void> {
|
|||||||
tracer,
|
tracer,
|
||||||
logger,
|
logger,
|
||||||
queue,
|
queue,
|
||||||
|
rateLimit: new NoopRateLimit(),
|
||||||
};
|
};
|
||||||
|
|
||||||
bindProductionAuth(ctx);
|
bindProductionAuth(ctx);
|
||||||
@@ -121,6 +123,7 @@ export async function bindAllDevSeed(): Promise<void> {
|
|||||||
tracer,
|
tracer,
|
||||||
logger,
|
logger,
|
||||||
queue,
|
queue,
|
||||||
|
rateLimit: new NoopRateLimit(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await bindDevSeedAuth(ctx);
|
await bindDevSeedAuth(ctx);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-20T08:43:15.868Z",
|
"generatedAt": "2026-05-20T08:59:30.996Z",
|
||||||
"commit": "a478a8e",
|
"commit": "24b2490",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 97.41,
|
"statements": 97.43,
|
||||||
"branches": 92.31,
|
"branches": 92.35,
|
||||||
"functions": 97.18,
|
"functions": 97.21,
|
||||||
"lines": 97.41,
|
"lines": 97.43,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 5867,
|
"lf": 5910,
|
||||||
"lh": 5715,
|
"lh": 5758,
|
||||||
"brf": 1184,
|
"brf": 1190,
|
||||||
"brh": 1093,
|
"brh": 1099,
|
||||||
"fnf": 354,
|
"fnf": 358,
|
||||||
"fnh": 344
|
"fnh": 348
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"byPackage": {
|
"byPackage": {
|
||||||
@@ -101,17 +101,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/core-shared": {
|
"@repo/core-shared": {
|
||||||
"statements": 98.15,
|
"statements": 98.21,
|
||||||
"branches": 96.12,
|
"branches": 96.19,
|
||||||
"functions": 92.73,
|
"functions": 92.98,
|
||||||
"lines": 98.15,
|
"lines": 98.21,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 1133,
|
"lf": 1176,
|
||||||
"lh": 1112,
|
"lh": 1155,
|
||||||
"brf": 335,
|
"brf": 341,
|
||||||
"brh": 322,
|
"brh": 328,
|
||||||
"fnf": 110,
|
"fnf": 114,
|
||||||
"fnh": 102
|
"fnh": 106
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/core-ui": {
|
"@repo/core-ui": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { attachBrand } from "@/conformance/brand-runtime";
|
|||||||
import type { ITracer, ISpan } from "@/instrumentation/tracer.interface";
|
import type { ITracer, ISpan } from "@/instrumentation/tracer.interface";
|
||||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||||
import type { BindContext } from "@/di/bind-context";
|
import type { BindContext } from "@/di/bind-context";
|
||||||
|
import type { RateLimitBudget } from "@/rate-limit/rate-limit.interface";
|
||||||
|
|
||||||
function makeTracer(): ITracer {
|
function makeTracer(): ITracer {
|
||||||
return {
|
return {
|
||||||
@@ -396,4 +397,126 @@ describe("assertFeatureConformance", () => {
|
|||||||
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws when use case declares rateLimit but binding is missing __rateLimited brand", () => {
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.signIn");
|
||||||
|
const ctx = makeCtx();
|
||||||
|
const wrappedNoRateLimit = withSpan(
|
||||||
|
ctx.tracer,
|
||||||
|
{ name: "test.signIn", op: "use-case" },
|
||||||
|
withCapture(
|
||||||
|
ctx.logger,
|
||||||
|
{ feature: "test", layer: "use-case" },
|
||||||
|
async (x: number) => x,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
container.bind(sym).toConstantValue(wrappedNoRateLimit);
|
||||||
|
|
||||||
|
const rateLimitBudget: RateLimitBudget = {
|
||||||
|
name: "global",
|
||||||
|
window: "1m",
|
||||||
|
budget: 60,
|
||||||
|
};
|
||||||
|
const manifest = defineFeature({
|
||||||
|
name: "test",
|
||||||
|
requiredCores: [],
|
||||||
|
useCases: {
|
||||||
|
signIn: {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
rateLimit: [rateLimitBudget],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realtimeChannels: [],
|
||||||
|
jobs: [],
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||||
|
).toThrow(ConformanceError);
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||||
|
).toThrow(/__rateLimited/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes when use case declares rateLimit and binding carries __rateLimited brand", () => {
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.signIn");
|
||||||
|
const ctx = makeCtx();
|
||||||
|
const base = withSpan(
|
||||||
|
ctx.tracer,
|
||||||
|
{ name: "test.signIn", op: "use-case" },
|
||||||
|
withCapture(
|
||||||
|
ctx.logger,
|
||||||
|
{ feature: "test", layer: "use-case" },
|
||||||
|
async (x: number) => x,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
attachBrand(base, "__rateLimited");
|
||||||
|
container.bind(sym).toConstantValue(base);
|
||||||
|
|
||||||
|
const rateLimitBudget: RateLimitBudget = {
|
||||||
|
name: "global",
|
||||||
|
window: "1m",
|
||||||
|
budget: 60,
|
||||||
|
};
|
||||||
|
const manifest = defineFeature({
|
||||||
|
name: "test",
|
||||||
|
requiredCores: [],
|
||||||
|
useCases: {
|
||||||
|
signIn: {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
rateLimit: [rateLimitBudget],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realtimeChannels: [],
|
||||||
|
jobs: [],
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes when rateLimit is empty and __rateLimited brand is absent", () => {
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.signIn");
|
||||||
|
const ctx = makeCtx();
|
||||||
|
const bound = withSpan(
|
||||||
|
ctx.tracer,
|
||||||
|
{ name: "test.signIn", op: "use-case" },
|
||||||
|
withCapture(
|
||||||
|
ctx.logger,
|
||||||
|
{ feature: "test", layer: "use-case" },
|
||||||
|
async (x: number) => x,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
container.bind(sym).toConstantValue(bound);
|
||||||
|
|
||||||
|
const manifest = defineFeature({
|
||||||
|
name: "test",
|
||||||
|
requiredCores: [],
|
||||||
|
useCases: {
|
||||||
|
signIn: {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
rateLimit: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realtimeChannels: [],
|
||||||
|
jobs: [],
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { signIn: sym }, ctx),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,66 @@
|
|||||||
import type { Container } from "inversify";
|
import type { Container } from "inversify";
|
||||||
import type { FeatureManifest } from "./define-feature";
|
import type { FeatureManifest, UseCaseManifest } from "./define-feature";
|
||||||
import {
|
import {
|
||||||
isInstrumented,
|
isInstrumented,
|
||||||
isCaptured,
|
isCaptured,
|
||||||
isAudited,
|
isAudited,
|
||||||
isAnalyzed,
|
isAnalyzed,
|
||||||
isConsentChecked,
|
isConsentChecked,
|
||||||
|
isRateLimited,
|
||||||
} from "./brand-runtime";
|
} from "./brand-runtime";
|
||||||
import { ConformanceError } from "./conformance-error";
|
import { ConformanceError } from "./conformance-error";
|
||||||
import type { BindContext } from "../di/bind-context";
|
import type { BindContext } from "../di/bind-context";
|
||||||
|
|
||||||
|
function requireBrand(ok: boolean, label: string, message: string): void {
|
||||||
|
if (!ok) throw new ConformanceError(`${label}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUseCaseBrands(
|
||||||
|
bound: unknown,
|
||||||
|
label: string,
|
||||||
|
useCase: UseCaseManifest,
|
||||||
|
requiresConsent: boolean,
|
||||||
|
): void {
|
||||||
|
requireBrand(
|
||||||
|
isInstrumented(bound),
|
||||||
|
label,
|
||||||
|
"missing __instrumented brand — was withSpan applied at bind time?",
|
||||||
|
);
|
||||||
|
requireBrand(
|
||||||
|
isCaptured(bound),
|
||||||
|
label,
|
||||||
|
"missing __captured brand — was withCapture applied at bind time?",
|
||||||
|
);
|
||||||
|
if (useCase.mutates && useCase.audits.length > 0) {
|
||||||
|
requireBrand(
|
||||||
|
isAudited(bound),
|
||||||
|
label,
|
||||||
|
"declares audits but binding is missing __audited brand — was withAudit applied at bind time?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ((useCase.analyticsEvents?.length ?? 0) > 0) {
|
||||||
|
requireBrand(
|
||||||
|
isAnalyzed(bound),
|
||||||
|
label,
|
||||||
|
"declares analyticsEvents but binding is missing __analyzed brand — was withAnalytics applied at bind time?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresConsent) {
|
||||||
|
requireBrand(
|
||||||
|
isConsentChecked(bound),
|
||||||
|
label,
|
||||||
|
"feature declares requiresConsent but binding is missing __consentChecked brand — was withConsent applied at bind time?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ((useCase.rateLimit?.length ?? 0) > 0) {
|
||||||
|
requireBrand(
|
||||||
|
isRateLimited(bound),
|
||||||
|
label,
|
||||||
|
"declares rateLimit but binding is missing __rateLimited brand — was withRateLimit applied at bind time?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime check that every manifest-declared use case is bound through the
|
* Runtime check that every manifest-declared use case is bound through the
|
||||||
* brand-attaching wrappers (`withSpan` → `__instrumented`,
|
* brand-attaching wrappers (`withSpan` → `__instrumented`,
|
||||||
@@ -32,6 +83,7 @@ export function assertFeatureConformance(
|
|||||||
_ctx: BindContext,
|
_ctx: BindContext,
|
||||||
): void {
|
): void {
|
||||||
void _ctx; // future: also check ctx.bus / ctx.auditLog presence vs requiredCores
|
void _ctx; // future: also check ctx.bus / ctx.auditLog presence vs requiredCores
|
||||||
|
const requiresConsent = (manifest.requiresConsent?.length ?? 0) > 0;
|
||||||
for (const [name, useCase] of Object.entries(manifest.useCases)) {
|
for (const [name, useCase] of Object.entries(manifest.useCases)) {
|
||||||
const sym = symbols[name];
|
const sym = symbols[name];
|
||||||
if (!sym) {
|
if (!sym) {
|
||||||
@@ -47,36 +99,11 @@ export function assertFeatureConformance(
|
|||||||
`${manifest.name}.${name}: container could not resolve symbol (${String(cause)})`,
|
`${manifest.name}.${name}: container could not resolve symbol (${String(cause)})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isInstrumented(bound)) {
|
checkUseCaseBrands(
|
||||||
throw new ConformanceError(
|
bound,
|
||||||
`${manifest.name}.${name}: missing __instrumented brand — was withSpan applied at bind time?`,
|
`${manifest.name}.${name}`,
|
||||||
|
useCase,
|
||||||
|
requiresConsent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isCaptured(bound)) {
|
|
||||||
throw new ConformanceError(
|
|
||||||
`${manifest.name}.${name}: missing __captured brand — was withCapture applied at bind time?`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (useCase.mutates && useCase.audits.length > 0) {
|
|
||||||
if (!isAudited(bound)) {
|
|
||||||
throw new ConformanceError(
|
|
||||||
`${manifest.name}.${name}: declares audits but binding is missing __audited brand — was withAudit applied at bind time?`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((useCase.analyticsEvents?.length ?? 0) > 0) {
|
|
||||||
if (!isAnalyzed(bound)) {
|
|
||||||
throw new ConformanceError(
|
|
||||||
`${manifest.name}.${name}: declares analyticsEvents but binding is missing __analyzed brand — was withAnalytics applied at bind time?`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((manifest.requiresConsent?.length ?? 0) > 0) {
|
|
||||||
if (!isConsentChecked(bound)) {
|
|
||||||
throw new ConformanceError(
|
|
||||||
`${manifest.name}.${name}: feature declares requiresConsent but binding is missing __consentChecked brand — was withConsent applied at bind time?`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
isCaptured,
|
isCaptured,
|
||||||
isAudited,
|
isAudited,
|
||||||
isAnalyzed,
|
isAnalyzed,
|
||||||
|
isRateLimited,
|
||||||
} from "@/conformance/brand-runtime";
|
} from "@/conformance/brand-runtime";
|
||||||
import type {
|
import type {
|
||||||
ITracer,
|
ITracer,
|
||||||
@@ -15,6 +16,7 @@ import type {
|
|||||||
} from "@/instrumentation/tracer.interface";
|
} from "@/instrumentation/tracer.interface";
|
||||||
import type { ILogger } from "@/instrumentation/logger.interface";
|
import type { ILogger } from "@/instrumentation/logger.interface";
|
||||||
import type { AuditLogProtocol, AnalyticsProtocol } from "@/di/bind-protocols";
|
import type { AuditLogProtocol, AnalyticsProtocol } from "@/di/bind-protocols";
|
||||||
|
import { NoopRateLimit } from "@/rate-limit/noop-rate-limit";
|
||||||
|
|
||||||
function makeTracer() {
|
function makeTracer() {
|
||||||
const calls: SpanOpts[] = [];
|
const calls: SpanOpts[] = [];
|
||||||
@@ -381,3 +383,130 @@ describe("wireUseCase — idempotent re-bind", () => {
|
|||||||
expect(container.get(sym)).toBe(second);
|
expect(container.get(sym)).toBe(second);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("wireUseCase — rate limit path", () => {
|
||||||
|
it("attaches RateLimited brand when rateLimit is provided", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const rateLimit = new NoopRateLimit();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
rateLimit,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isRateLimited(wired)).toBe(true);
|
||||||
|
expect(isInstrumented(wired)).toBe(true);
|
||||||
|
expect(isCaptured(wired)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not attach RateLimited brand when rateLimit is absent", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isRateLimited(wired)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("executes factory correctly when rateLimit is provided", async () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const rateLimit = new NoopRateLimit();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
rateLimit,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(wired(5)).resolves.toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attaches RateLimited + Analyzed brands when both rateLimit and analytics are provided", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const rateLimit = new NoopRateLimit();
|
||||||
|
const analytics = makeAnalytics();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
rateLimit,
|
||||||
|
analytics,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isRateLimited(wired)).toBe(true);
|
||||||
|
expect(isAnalyzed(wired)).toBe(true);
|
||||||
|
expect(isInstrumented(wired)).toBe(true);
|
||||||
|
expect(isCaptured(wired)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attaches RateLimited + Audited brands when both rateLimit and auditLog are provided", () => {
|
||||||
|
const { tracer } = makeTracer();
|
||||||
|
const logger = makeLogger();
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.double");
|
||||||
|
const rateLimit = new NoopRateLimit();
|
||||||
|
const auditLog = makeAuditLog();
|
||||||
|
|
||||||
|
const wired = wireUseCase({
|
||||||
|
container,
|
||||||
|
symbol: sym,
|
||||||
|
factory: doubleFactory,
|
||||||
|
deps: [],
|
||||||
|
feature: "test",
|
||||||
|
layer: "use-case",
|
||||||
|
name: "double",
|
||||||
|
tracer,
|
||||||
|
logger,
|
||||||
|
rateLimit,
|
||||||
|
auditLog,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isRateLimited(wired)).toBe(true);
|
||||||
|
expect(isAudited(wired)).toBe(true);
|
||||||
|
expect(isInstrumented(wired)).toBe(true);
|
||||||
|
expect(isCaptured(wired)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import type { Container } from "inversify";
|
|||||||
import type { ITracer } from "../instrumentation/tracer.interface";
|
import type { ITracer } from "../instrumentation/tracer.interface";
|
||||||
import type { ILogger } from "../instrumentation/logger.interface";
|
import type { ILogger } from "../instrumentation/logger.interface";
|
||||||
import type { AuditLogProtocol, AnalyticsProtocol } from "../di/bind-protocols";
|
import type { AuditLogProtocol, AnalyticsProtocol } from "../di/bind-protocols";
|
||||||
|
import type { IRateLimit } from "../rate-limit/rate-limit.interface";
|
||||||
import { withSpan } from "../instrumentation/with-span";
|
import { withSpan } from "../instrumentation/with-span";
|
||||||
import { withCapture } from "../instrumentation/with-capture";
|
import { withCapture } from "../instrumentation/with-capture";
|
||||||
|
import { withRateLimit } from "../rate-limit/with-rate-limit";
|
||||||
import { attachBrand } from "./brand-runtime";
|
import { attachBrand } from "./brand-runtime";
|
||||||
|
|
||||||
export type WireUseCaseOptions<
|
export type WireUseCaseOptions<
|
||||||
@@ -22,6 +24,7 @@ export type WireUseCaseOptions<
|
|||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
analytics?: AnalyticsProtocol;
|
analytics?: AnalyticsProtocol;
|
||||||
auditLog?: AuditLogProtocol;
|
auditLog?: AuditLogProtocol;
|
||||||
|
rateLimit?: IRateLimit;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +56,7 @@ export function wireUseCase<
|
|||||||
logger,
|
logger,
|
||||||
analytics,
|
analytics,
|
||||||
auditLog,
|
auditLog,
|
||||||
|
rateLimit,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const spanName = `${feature}.${name}`;
|
const spanName = `${feature}.${name}`;
|
||||||
@@ -60,17 +64,27 @@ export function wireUseCase<
|
|||||||
|
|
||||||
const raw = factory(...deps);
|
const raw = factory(...deps);
|
||||||
|
|
||||||
let toWrap: (...args: FnArgs) => Promise<R>;
|
// rateLimit is innermost — wraps raw before analytics/audit. Attaches
|
||||||
// analytics is innermost — wraps raw before audit. withAnalytics lives in
|
// __rateLimited so withCapture + withSpan can propagate it to the outermost binding.
|
||||||
|
let toWrap: (...args: FnArgs) => Promise<R> =
|
||||||
|
rateLimit !== undefined ? withRateLimit(rateLimit, raw) : raw;
|
||||||
|
|
||||||
|
// analytics wraps rateLimit (or raw) before audit. withAnalytics lives in
|
||||||
// @repo/core-analytics which depends on core-shared (not vice versa), so we
|
// @repo/core-analytics which depends on core-shared (not vice versa), so we
|
||||||
// replicate the forwarding-wrapper semantics inline to avoid a circular dep.
|
// replicate the forwarding-wrapper semantics inline to avoid a circular dep.
|
||||||
if (analytics !== undefined) {
|
if (analytics !== undefined) {
|
||||||
void analytics; // reserved for future automated event recording from manifest declarations
|
void analytics; // reserved for future automated event recording from manifest declarations
|
||||||
const analyzed: (...args: FnArgs) => Promise<R> = (...args) => raw(...args);
|
const prev = toWrap;
|
||||||
|
const analyzed: (...args: FnArgs) => Promise<R> = (...args) =>
|
||||||
|
prev(...args);
|
||||||
attachBrand(analyzed, "__analyzed");
|
attachBrand(analyzed, "__analyzed");
|
||||||
|
// propagate __rateLimited from inner so withCapture/withSpan see it
|
||||||
|
if (
|
||||||
|
(prev as unknown as Record<string, unknown>)["__rateLimited"] === true
|
||||||
|
) {
|
||||||
|
attachBrand(analyzed, "__rateLimited");
|
||||||
|
}
|
||||||
toWrap = analyzed;
|
toWrap = analyzed;
|
||||||
} else {
|
|
||||||
toWrap = raw;
|
|
||||||
}
|
}
|
||||||
if (auditLog !== undefined) {
|
if (auditLog !== undefined) {
|
||||||
void auditLog; // reserved for future automated audit recording from manifest declarations
|
void auditLog; // reserved for future automated audit recording from manifest declarations
|
||||||
@@ -78,11 +92,16 @@ export function wireUseCase<
|
|||||||
const prev = toWrap;
|
const prev = toWrap;
|
||||||
const audited: (...args: FnArgs) => Promise<R> = (...args) => prev(...args);
|
const audited: (...args: FnArgs) => Promise<R> = (...args) => prev(...args);
|
||||||
attachBrand(audited, "__audited");
|
attachBrand(audited, "__audited");
|
||||||
// propagate __analyzed from the analytics layer below so withCapture/withSpan
|
// propagate __analyzed and __rateLimited from inner so withCapture/withSpan
|
||||||
// can see it on the outermost binding
|
// can see them on the outermost binding
|
||||||
if ((prev as unknown as Record<string, unknown>)["__analyzed"] === true) {
|
if ((prev as unknown as Record<string, unknown>)["__analyzed"] === true) {
|
||||||
attachBrand(audited, "__analyzed");
|
attachBrand(audited, "__analyzed");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
(prev as unknown as Record<string, unknown>)["__rateLimited"] === true
|
||||||
|
) {
|
||||||
|
attachBrand(audited, "__rateLimited");
|
||||||
|
}
|
||||||
toWrap = audited;
|
toWrap = audited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { SanitizedConfig } from "payload";
|
import type { SanitizedConfig } from "payload";
|
||||||
import type { ITracer, ILogger } from "../instrumentation";
|
import type { ITracer, ILogger } from "../instrumentation";
|
||||||
import type { IJobQueue } from "../jobs";
|
import type { IJobQueue } from "../jobs";
|
||||||
|
import type { IRateLimit } from "../rate-limit/rate-limit.interface";
|
||||||
import type {
|
import type {
|
||||||
EventBusProtocol,
|
EventBusProtocol,
|
||||||
RealtimeBroadcasterProtocol,
|
RealtimeBroadcasterProtocol,
|
||||||
@@ -45,6 +46,7 @@ export type BindContext<
|
|||||||
auditLog?: Audit;
|
auditLog?: Audit;
|
||||||
analytics?: Analytics;
|
analytics?: Analytics;
|
||||||
consentFactory?: ConsentFactoryProtocol;
|
consentFactory?: ConsentFactoryProtocol;
|
||||||
|
rateLimit?: IRateLimit;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Production binders also receive the resolved Payload config. */
|
/** Production binders also receive the resolved Payload config. */
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function withCapture<Args extends unknown[], R>(
|
|||||||
"__audited",
|
"__audited",
|
||||||
"__analyzed",
|
"__analyzed",
|
||||||
"__consentChecked",
|
"__consentChecked",
|
||||||
|
"__rateLimited",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const PROPAGATED_BRANDS = [
|
|||||||
"__audited",
|
"__audited",
|
||||||
"__analyzed",
|
"__analyzed",
|
||||||
"__consentChecked",
|
"__consentChecked",
|
||||||
|
"__rateLimited",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export type {
|
|||||||
} from "./rate-limit.interface";
|
} from "./rate-limit.interface";
|
||||||
export { NoopRateLimit } from "./noop-rate-limit";
|
export { NoopRateLimit } from "./noop-rate-limit";
|
||||||
export { InMemoryRateLimit } from "./in-memory-rate-limit";
|
export { InMemoryRateLimit } from "./in-memory-rate-limit";
|
||||||
|
export { withRateLimit } from "./with-rate-limit";
|
||||||
|
|||||||
85
packages/core-shared/src/rate-limit/with-rate-limit.test.ts
Normal file
85
packages/core-shared/src/rate-limit/with-rate-limit.test.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it, expect, expectTypeOf } from "vitest";
|
||||||
|
import { withRateLimit } from "@/rate-limit/with-rate-limit";
|
||||||
|
import {
|
||||||
|
isRateLimited,
|
||||||
|
isInstrumented,
|
||||||
|
isCaptured,
|
||||||
|
} from "@/conformance/brand-runtime";
|
||||||
|
import { attachBrand } from "@/conformance/brand-runtime";
|
||||||
|
import type { RateLimited } from "@/conformance/brands";
|
||||||
|
import { NoopRateLimit } from "@/rate-limit/noop-rate-limit";
|
||||||
|
|
||||||
|
function makeRateLimit() {
|
||||||
|
return new NoopRateLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("withRateLimit — brand", () => {
|
||||||
|
it("returns a RateLimited<F> type", () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const fn = async (a: number) => a + 1;
|
||||||
|
const wrapped = withRateLimit(rateLimit, fn);
|
||||||
|
expectTypeOf(wrapped).toMatchTypeOf<RateLimited<typeof fn>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attaches __rateLimited as a non-enumerable property", () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const wrapped = withRateLimit(rateLimit, async (x: number) => x + 1);
|
||||||
|
expect(isRateLimited(wrapped)).toBe(true);
|
||||||
|
expect(Object.keys(wrapped)).not.toContain("__rateLimited");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withRateLimit — factory passthrough", () => {
|
||||||
|
it("forwards arguments and return value unchanged on success", async () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const fn = async (a: number, b: number) => a + b;
|
||||||
|
const wrapped = withRateLimit(rateLimit, fn);
|
||||||
|
await expect(wrapped(3, 4)).resolves.toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-throws errors from the inner function", async () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const err = new Error("inner failure");
|
||||||
|
const fn = async () => {
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
const wrapped = withRateLimit(rateLimit, fn);
|
||||||
|
await expect(wrapped()).rejects.toBe(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withRateLimit — does not attach other brands", () => {
|
||||||
|
it("does not attach __instrumented or __captured", () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const wrapped = withRateLimit(rateLimit, async (x: number) => x);
|
||||||
|
expect(isInstrumented(wrapped)).toBe(false);
|
||||||
|
expect(isCaptured(wrapped)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withRateLimit — composition with other wrappers", () => {
|
||||||
|
it("composed result carries __rateLimited when withRateLimit is innermost", () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const raw = async (x: number) => x * 2;
|
||||||
|
const rateLimited = withRateLimit(rateLimit, raw);
|
||||||
|
|
||||||
|
// Simulate outer wrapper propagating __rateLimited
|
||||||
|
const outer: typeof raw = (...args) => rateLimited(...args);
|
||||||
|
if (
|
||||||
|
(rateLimited as unknown as Record<string, unknown>)["__rateLimited"] ===
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
attachBrand(outer, "__rateLimited");
|
||||||
|
}
|
||||||
|
expect(isRateLimited(outer)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("original fn reference is not mutated", () => {
|
||||||
|
const rateLimit = makeRateLimit();
|
||||||
|
const fn = async (x: number) => x;
|
||||||
|
const wrapped = withRateLimit(rateLimit, fn);
|
||||||
|
expect(wrapped).not.toBe(fn);
|
||||||
|
expect(isRateLimited(fn)).toBe(false);
|
||||||
|
expect(isRateLimited(wrapped)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
packages/core-shared/src/rate-limit/with-rate-limit.ts
Normal file
25
packages/core-shared/src/rate-limit/with-rate-limit.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { IRateLimit } from "./rate-limit.interface";
|
||||||
|
import type { RateLimited } from "../conformance/brands";
|
||||||
|
import { attachBrand } from "../conformance/brand-runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use-case wrapper applied at DI bind time. Attaches the `__rateLimited`
|
||||||
|
* brand so the boot-time assertion can verify rate-limited use cases were
|
||||||
|
* bound through the rate-limit-aware path.
|
||||||
|
*
|
||||||
|
* The forward closure keeps the brand on a fresh function so the original
|
||||||
|
* `fn` reference is not mutated — important when the same factory output is
|
||||||
|
* used elsewhere unwrapped (dev-seed paths, tests).
|
||||||
|
*
|
||||||
|
* Composition order (outermost to innermost):
|
||||||
|
* withSpan → withCapture → withAudit → withAnalytics → withRateLimit → factory(deps)
|
||||||
|
*/
|
||||||
|
export function withRateLimit<Args extends unknown[], R>(
|
||||||
|
rateLimit: IRateLimit,
|
||||||
|
fn: (...args: Args) => Promise<R>,
|
||||||
|
): RateLimited<(...args: Args) => Promise<R>> {
|
||||||
|
void rateLimit; // future: enforce rate limit at runtime per manifest budgets
|
||||||
|
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
||||||
|
attachBrand(wrapped, "__rateLimited");
|
||||||
|
return wrapped as RateLimited<(...args: Args) => Promise<R>>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user