Initial Commit
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
definePlugin,
|
||||
type PluginContext,
|
||||
type PluginDescriptor,
|
||||
} from "@suaveplan/api-plugin";
|
||||
import type { Clock } from "@suaveplan/types";
|
||||
import {
|
||||
RATE_LIMIT_PLUGIN_CODES,
|
||||
RateLimitPluginError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { rateLimit } from "../middleware/rate-limit.js";
|
||||
import { RateLimitPluginOptionsSchema } from "../schemas/options.js";
|
||||
import {
|
||||
createDefaultStorage,
|
||||
createRateLimitService,
|
||||
type RateLimitService,
|
||||
} from "../services/rate-limit-service.js";
|
||||
import "../services/registry-augmentation.js";
|
||||
import type {
|
||||
RateLimitPluginConfig,
|
||||
RateLimitPluginOptions,
|
||||
} from "../types/types.js";
|
||||
|
||||
/**
|
||||
* Internal state owned by the plugin instance across the 4-phase
|
||||
* lifecycle. Captured as a closure variable rather than on
|
||||
* `PluginContext` because the registry's per-hook context is rebuilt
|
||||
* each call.
|
||||
*/
|
||||
interface RateLimitPluginState {
|
||||
service: RateLimitService | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory — produces a typed `PluginDescriptor` for the rate-limit
|
||||
* plugin.
|
||||
*
|
||||
* Zod-validates the supplied options at the boundary so invalid input
|
||||
* fails fast with `RateLimitPluginError(INVALID_INPUT)` carrying the
|
||||
* `ZodError` as `cause`. The returned descriptor's `onInit` executes
|
||||
* the canonical 6-step lifecycle:
|
||||
*
|
||||
* 1. Resolve `app` + `clock` + `logger` from the host
|
||||
* `ServiceRegistry`.
|
||||
* 2. Build the `RateLimitStorage` (caller-supplied or the bundled
|
||||
* `MemoryStorage`).
|
||||
* 3. Construct the `RateLimitService` bound to the resolved config.
|
||||
* 4. Register `rateLimitService` in the `ServiceRegistry`.
|
||||
* 5. Install the global `rateLimit` middleware on `ctx.app` so every
|
||||
* request is rate-limited by default.
|
||||
* 6. Emit a single info log so operators see the plugin came online.
|
||||
*/
|
||||
export function createRateLimitPlugin(
|
||||
options: RateLimitPluginOptions
|
||||
): PluginDescriptor<RateLimitPluginConfig> {
|
||||
const parsed = RateLimitPluginOptionsSchema.safeParse(options);
|
||||
if (!parsed.success) {
|
||||
throw new RateLimitPluginError(
|
||||
RATE_LIMIT_PLUGIN_CODES.INVALID_INPUT,
|
||||
"createRateLimitPlugin received invalid options",
|
||||
{ cause: parsed.error, context: { statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
const resolved = parsed.data;
|
||||
const state: RateLimitPluginState = { service: null };
|
||||
|
||||
return definePlugin<RateLimitPluginConfig>({
|
||||
name: "rate-limit",
|
||||
version: "0.1.0",
|
||||
description:
|
||||
"Rate-limit plugin for @suaveplan/api. Wraps @suaveplan/rate-limiter with per-route + per-tenant configuration, IP whitelist, parameterised path matching, Hono middleware, and X-RateLimit-* / Retry-After headers.",
|
||||
defaults: resolved,
|
||||
tags: ["rate-limit", "security"],
|
||||
onInit: async (ctx) => runOnInit(ctx, resolved, state),
|
||||
onClose: async () => {
|
||||
state.service = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function runOnInit(
|
||||
ctx: PluginContext<RateLimitPluginConfig>,
|
||||
config: RateLimitPluginConfig,
|
||||
state: RateLimitPluginState
|
||||
): Promise<void> {
|
||||
// (1) Resolve required services from the host ServiceRegistry.
|
||||
const app = ctx.getService("app");
|
||||
if (app === undefined) {
|
||||
throw new RateLimitPluginError(
|
||||
RATE_LIMIT_PLUGIN_CODES.SERVICE_NOT_AVAILABLE,
|
||||
"rate-limit plugin requires the `app` service in ServiceRegistry",
|
||||
{ context: { missing: ["app"], statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
const clock: Clock = ctx.getService("clock") ?? (() => Date.now());
|
||||
const logger = config.logger ?? ctx.logger;
|
||||
|
||||
// (2) Build storage.
|
||||
const storage = config.storage ?? createDefaultStorage({ clock });
|
||||
|
||||
// (3) Construct the service.
|
||||
const service = createRateLimitService({
|
||||
config,
|
||||
storage,
|
||||
clock,
|
||||
logger,
|
||||
});
|
||||
state.service = service;
|
||||
|
||||
// (4) Register the service slot.
|
||||
ctx.registerService("rateLimitService", service);
|
||||
|
||||
// (5) Install the global middleware.
|
||||
app.use("*", rateLimit({ service }));
|
||||
|
||||
// (6) Log readiness.
|
||||
logger.info("rate-limit plugin: ready", {
|
||||
defaultLimit: config.defaultLimit,
|
||||
defaultWindowMs: config.defaultWindowMs,
|
||||
routes: config.routes.length,
|
||||
tenants: config.tenants.length,
|
||||
ipWhitelist: config.ipWhitelist.length,
|
||||
headerStyle: config.headerStyle,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user