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:
2026-05-20 09:00:31 +00:00
parent 24b2490d86
commit cb61f51ee1
12 changed files with 477 additions and 61 deletions

View File

@@ -5,3 +5,4 @@ export type {
} from "./rate-limit.interface";
export { NoopRateLimit } from "./noop-rate-limit";
export { InMemoryRateLimit } from "./in-memory-rate-limit";
export { withRateLimit } from "./with-rate-limit";

View 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);
});
});

View 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>>;
}