Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { registerRateLimitServiceSlot } from "../src/services/registry-augmentation.js";
const here = dirname(fileURLToPath(import.meta.url));
const pkgRoot = join(here, "..");
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
describe("dep-graph", () => {
it("ships ten subpath exports including the root", {
timeout: 5000,
}, () => {
const exportKeys = Object.keys(pkg.exports).filter(
(k: string) => k !== "./package.json"
);
expect(exportKeys.length).toBeGreaterThanOrEqual(10);
});
it("declares the canonical runtime deps", { timeout: 5000 }, () => {
const deps = pkg.dependencies as Record<string, string>;
expect(deps["@suaveplan/api-plugin"]).toBe("workspace:^");
expect(deps["@suaveplan/rate-limiter"]).toBe("workspace:^");
expect(deps["@suaveplan/error"]).toBe("workspace:^");
expect(deps["@suaveplan/types"]).toBe("workspace:^");
expect(deps.hono).toBe("catalog:");
expect(deps.zod).toBe("catalog:forms");
});
function walk(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
if (statSync(p).isDirectory()) walk(p, out);
else if (
entry.endsWith(".ts") &&
!entry.endsWith(".test.ts") &&
!entry.endsWith(".types.ts") &&
entry !== "index.ts"
)
out.push(p);
}
return out;
}
it("registerRateLimitServiceSlot is callable without throwing", {
timeout: 5000,
}, () => {
expect(() => registerRateLimitServiceSlot()).not.toThrow();
});
it("every implementation .ts has a sibling .md", { timeout: 5000 }, () => {
const sources = walk(join(pkgRoot, "src"));
const missing: string[] = [];
for (const ts of sources) {
const md = ts.replace(/\.ts$/, ".md");
try {
statSync(md);
} catch {
missing.push(md);
}
}
expect(missing).toEqual([]);
});
});

View File

@@ -0,0 +1,164 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { createPluginContext } from "@suaveplan/api-plugin";
import { EventBus } from "@suaveplan/events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createRateLimitPlugin } from "../../src/plugin/plugin.js";
import type { RateLimitPluginConfig } from "../../src/types/types.js";
const noopLogger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
async function bootApp(extra: Partial<RateLimitPluginConfig> = {}) {
const plugin = createRateLimitPlugin({
defaultLimit: 3,
defaultWindowMs: 60_000,
routes: [
{
path: "/api/login",
method: "POST",
limit: 1,
windowMs: 60_000,
id: "login",
},
],
tenants: [
{ tenantId: "ent", multiplier: 5 },
{ tenantId: "internal", bypass: true },
],
...extra,
});
const app = new OpenAPIHono();
const services = new Map<string, unknown>();
services.set("app", app);
const ctx = createPluginContext<RateLimitPluginConfig>({
logger: noopLogger,
eventBus: new EventBus(),
signal: new AbortController().signal,
config: plugin.defaults as RateLimitPluginConfig,
services,
});
await plugin.onInit?.(ctx);
app.get("/api/users", (c) => c.text("ok"));
app.post("/api/login", (c) => c.text("ok"));
return { app, plugin };
}
describe("integration — full app", () => {
it("isolates rate-limit counters per tenant", {
timeout: 30_000,
}, async () => {
const { app } = await bootApp();
// Tenant A exhausts a defaultLimit of 3
for (let i = 0; i < 3; i++) {
const res = await app.request(
new Request("http://x/api/users", {
headers: {
"x-forwarded-for": "1.1.1.1",
"x-tenant-id": "A",
},
})
);
expect(res.status).toBe(200);
}
const denied = await app.request(
new Request("http://x/api/users", {
headers: {
"x-forwarded-for": "1.1.1.1",
"x-tenant-id": "A",
},
})
);
expect(denied.status).toBe(429);
// Tenant B still has its own counter
const bRes = await app.request(
new Request("http://x/api/users", {
headers: {
"x-forwarded-for": "1.1.1.1",
"x-tenant-id": "B",
},
})
);
expect(bRes.status).toBe(200);
expect(bRes.headers.get("X-RateLimit-Remaining")).toBe("2");
});
it("honours per-route limit (login = 1 per 60s)", {
timeout: 30_000,
}, async () => {
const { app } = await bootApp();
const first = await app.request(
new Request("http://x/api/login", {
method: "POST",
headers: { "x-forwarded-for": "2.2.2.2" },
})
);
expect(first.status).toBe(200);
const second = await app.request(
new Request("http://x/api/login", {
method: "POST",
headers: { "x-forwarded-for": "2.2.2.2" },
})
);
expect(second.status).toBe(429);
expect(second.headers.get("Retry-After")).toBeTruthy();
});
it("applies tenant multiplier to the limit", {
timeout: 30_000,
}, async () => {
const { app } = await bootApp();
// ent has multiplier 5 on a default of 3 -> 15 allowed
let lastStatus = 0;
for (let i = 0; i < 15; i++) {
const res = await app.request(
new Request("http://x/api/users", {
headers: {
"x-forwarded-for": "3.3.3.3",
"x-tenant-id": "ent",
},
})
);
lastStatus = res.status;
}
expect(lastStatus).toBe(200);
const denied = await app.request(
new Request("http://x/api/users", {
headers: {
"x-forwarded-for": "3.3.3.3",
"x-tenant-id": "ent",
},
})
);
expect(denied.status).toBe(429);
});
it("bypasses internal tenant entirely", { timeout: 30_000 }, async () => {
const { app } = await bootApp({ defaultLimit: 1 });
for (let i = 0; i < 25; i++) {
const res = await app.request(
new Request("http://x/api/users", {
headers: {
"x-forwarded-for": "4.4.4.4",
"x-tenant-id": "internal",
},
})
);
expect(res.status).toBe(200);
}
});
it("emits headers on every response", { timeout: 30_000 }, async () => {
const { app } = await bootApp({ headerStyle: "both" });
const res = await app.request(
new Request("http://x/api/users", {
headers: { "x-forwarded-for": "5.5.5.5" },
})
);
expect(res.headers.get("X-RateLimit-Limit")).toBeTruthy();
expect(res.headers.get("RateLimit-Limit")).toBeTruthy();
});
});

View File

@@ -0,0 +1,32 @@
/**
* Test setup for @suaveplan/api-plugin-rate-limit.
*
* Installs a real in-memory OpenTelemetry MeterProvider BEFORE any source
* module imports so the meter resolved on first call binds to it.
*/
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter: testInMemoryMetricExporter,
exportIntervalMillis: 60_000,
});
export const testMeterProvider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(testMeterProvider);
export async function flushTestMetrics() {
await testMeterProvider.forceFlush();
const collected = testInMemoryMetricExporter.getMetrics();
testInMemoryMetricExporter.reset();
return collected;
}