@kanjijs/throttler 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @kanjijs/throttler
2
+
3
+ Rate limiting module for the Kanjijs Framework. Protects your application from brute-force attacks and abuse.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @kanjijs/throttler
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Global Configuration
14
+
15
+ Register the module in your root `AppModule`. This will apply the limit to **ALL** routes by default.
16
+
17
+ ```typescript
18
+ import { Module } from "@kanjijs/core";
19
+ import { ThrottlerModule } from "@kanjijs/throttler";
20
+
21
+ @Module({
22
+ imports: [
23
+ ThrottlerModule.forRoot({
24
+ limit: 10, // Max requests
25
+ ttl: 60 // Time window in seconds
26
+ })
27
+ ]
28
+ })
29
+ export class AppModule {}
30
+ ```
31
+
32
+ ### 2. Customizing Specific Routes
33
+
34
+ Use the `@Throttle` decorator to override the global settings for specific methods or controllers.
35
+
36
+ ```typescript
37
+ import { Controller, Get } from "@kanjijs/core";
38
+ import { Throttle } from "@kanjijs/throttler";
39
+
40
+ @Controller("/files")
41
+ export class FileController {
42
+
43
+ @Get("/download")
44
+ @Throttle({ limit: 3, ttl: 3600 }) // Stricter limit: 3 downloads per hour
45
+ downloadFile() {
46
+ return "File content...";
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Features
52
+
53
+ - **Global Protection**: Acts as a Global Middleware automatically registered via `KanjijsAdapter`.
54
+ - **Headers**: Automatically adds standard rate limit headers:
55
+ - `X-RateLimit-Limit`
56
+ - `X-RateLimit-Remaining`
57
+ - `X-RateLimit-Reset`
58
+ - `Retry-After` (on 429)
59
+ - **Storage**: Currently supports In-Memory storage. Redis support coming soon.
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@kanjijs/throttler",
3
+ "version": "0.2.0-beta.1",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "dependencies": {
7
+ "@kanjijs/core": "workspace:*",
8
+ "hono": "^4.0.0",
9
+ "reflect-metadata": "^0.2.0"
10
+ },
11
+ "scripts": {
12
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun"
13
+ }
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ console.log("Throttler Package Loaded");
2
+ export * from "./throttler.module";
3
+ export * from "./throttler.service";
4
+ export * from "./throttler.decorator";
5
+ export * from "./throttler.storage";
6
+ export * from "./throttler.guard";
@@ -0,0 +1,27 @@
1
+ import { Use, KanjijsIoC } from "@kanjijs/core";
2
+ import { type Context, type Next } from "hono";
3
+ import { ThrottlerService } from "./throttler.service";
4
+
5
+ export interface ThrottlerOptions {
6
+ limit: number;
7
+ ttl: number; // seconds
8
+ }
9
+
10
+ export function Throttle(options: ThrottlerOptions) {
11
+ return Use(async (c: Context, next: Next) => {
12
+ // Resolve singleton
13
+ const throttler = KanjijsIoC.resolve(ThrottlerService);
14
+
15
+ if (throttler) {
16
+ // Use specific options from decorator
17
+ const allowed = await throttler.handleRequest(c, options.limit, options.ttl);
18
+ if (!allowed) return; // Blocked (handleRequest sets status/body)
19
+ } else {
20
+ // Fallback or warning? If module is not imported, throttler service won't exist.
21
+ // It's safe to just next() or warn.
22
+ console.warn("@Throttle used but ThrottlerModule not imported/provided.");
23
+ }
24
+
25
+ await next();
26
+ });
27
+ }
@@ -0,0 +1,85 @@
1
+ import { Inject, Injectable, GLOBAL_MIDDLEWARE_TOKEN } from "@kanjijs/core";
2
+ import { type Context, type Next } from "hono";
3
+ import { ThrottlerInMemoryStorage, type ThrottlerStorage } from "./throttler.storage";
4
+ import { THROTTLER_LIMIT, THROTTLER_TTL } from "./throttler.decorator";
5
+
6
+ export interface ThrottlerModuleOptions {
7
+ limit?: number;
8
+ ttl?: number;
9
+ storage?: ThrottlerStorage;
10
+ }
11
+
12
+ export const THROTTLER_OPTIONS = "THROTTLER_OPTIONS";
13
+
14
+ @Injectable()
15
+ export class ThrottlerGuard {
16
+ private storage: ThrottlerStorage;
17
+ private defaultLimit: number;
18
+ private defaultTtl: number;
19
+
20
+ constructor(@Inject(THROTTLER_OPTIONS) private options: ThrottlerModuleOptions) {
21
+ this.storage = options.storage || new ThrottlerInMemoryStorage();
22
+ this.defaultLimit = options.limit || 10;
23
+ this.defaultTtl = options.ttl || 60;
24
+ }
25
+
26
+ // This will be registered as a Global Middleware in KanjijsAdapter if provided
27
+ async handle(c: Context, next: Next) {
28
+ // 1. Identify Client (IP)
29
+ // Hono "conn" info or header
30
+ /*
31
+ NOTE: In Bun/Hono, getting IP can be platform specific.
32
+ We will trust X-Forwarded-For or use a fallback.
33
+ */
34
+ const ip = c.req.header("x-forwarded-for") || "unknown";
35
+ const key = `throttler:${ip}:${c.req.path}`; // Limit by IP + Path (or just IP?) -> Usually just IP for global
36
+
37
+ // 2. Resolve Metadata (Limit/TTL)
38
+ // KanjijsAdapter doesn't expose the "Current Handler" to global middleware easily yet in the context.
39
+ // LIMITATION: Global Middleware runs BEFORE routing in Hono standard app.use('*').
40
+ // To support @Throttle decorators, we need to run THIS check inside the route handler wrapper OR
41
+ // we need to access the route handler metadata here.
42
+
43
+ // CURRENT STRATEGY:
44
+ // Global Config applies to ALL routes globally (Middleware style).
45
+ // @Throttle decorator support requires Per-Route logic.
46
+ // Since KanjijsAdapter registers routes, we should probably implement this as a specific middleware wrapper there,
47
+ // OR we rely on Hono context to pass handler info? No standard way.
48
+
49
+ // REVISED PLAN:
50
+ // We will implement `ThrottlerGuard` but we cannot easily use it as a simple `app.use('*')`
51
+ // IF we want to support @Throttle decorators overriding it properly *per route* efficiently without finding the route match twice.
52
+
53
+ // HOWEVER, for MVP:
54
+ // 1. Global Throttling: Runs on `*`, uses default limit.
55
+ // 2. Decorator Throttling: The adapter needs to know about it.
56
+
57
+ // BUT the user asked for: "rate limit haz que tambien se pueda configurar desde main y no solo usando el decorador"
58
+
59
+ // Let's stick to the plan:
60
+ // If we use Global Middleware, we enforce global limit.
61
+ // If we want specific limits, we need `KanjijsAdapter` to help us OR we attach `ThrottlerGuard` to the route in the Adapter.
62
+
63
+ // DECISION:
64
+ // This `handle` method will apply the DEFAULT global limit if installed globally.
65
+ // For Decorator support, `KanjijsAdapter` needs to look for `@Throttle` metadata and add THIS middleware (configured differently) to that route.
66
+
67
+ const limit = this.defaultLimit;
68
+ const ttl = this.defaultTtl;
69
+
70
+ const { total, timeToExpire } = await this.storage.increment(key, ttl);
71
+
72
+ if (total > limit) {
73
+ return c.text(`Too Many Requests`, 429, {
74
+ "Retry-After": timeToExpire.toString()
75
+ });
76
+ }
77
+
78
+ // Headers
79
+ c.header("X-RateLimit-Limit", limit.toString());
80
+ c.header("X-RateLimit-Remaining", Math.max(0, limit - total).toString());
81
+ c.header("X-RateLimit-Reset", timeToExpire.toString());
82
+
83
+ await next();
84
+ }
85
+ }
@@ -0,0 +1,36 @@
1
+ import { Module, GLOBAL_MIDDLEWARE_TOKEN, KanjijsIoC } from "@kanjijs/core";
2
+ import type { Context, Next } from "hono";
3
+ import { ThrottlerService, THROTTLER_OPTIONS, type ThrottlerModuleOptions } from "./throttler.service";
4
+
5
+ @Module({})
6
+ export class ThrottlerModule {
7
+ static forRoot(options: ThrottlerModuleOptions) {
8
+ return {
9
+ module: ThrottlerModule,
10
+ providers: [
11
+ {
12
+ provide: THROTTLER_OPTIONS,
13
+ useValue: options,
14
+ },
15
+ ThrottlerService,
16
+ {
17
+ provide: GLOBAL_MIDDLEWARE_TOKEN,
18
+ useValue: async (c: Context, next: Next) => {
19
+ // console.log("Throttler Middleware Executing via Global Token");
20
+ const throttler = KanjijsIoC.resolve(ThrottlerService);
21
+ if (throttler) {
22
+ const { limit, ttl } = throttler.getGlobalOptions();
23
+ const allowed = await throttler.handleRequest(c, limit, ttl);
24
+ if (!allowed) {
25
+ c.header("Retry-After", ttl.toString());
26
+ return c.text("Too Many Requests", 429);
27
+ }
28
+ }
29
+ await next();
30
+ }
31
+ }
32
+ ],
33
+ exports: [ThrottlerService],
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,50 @@
1
+ import { Inject, Injectable } from "@kanjijs/core";
2
+ import { type Context } from "hono";
3
+ import { ThrottlerInMemoryStorage, type ThrottlerStorage } from "./throttler.storage";
4
+
5
+ export interface ThrottlerModuleOptions {
6
+ limit?: number;
7
+ ttl?: number;
8
+ storage?: ThrottlerStorage;
9
+ }
10
+
11
+ export const THROTTLER_OPTIONS = "THROTTLER_OPTIONS";
12
+
13
+ @Injectable()
14
+ export class ThrottlerService {
15
+ private storage: ThrottlerStorage;
16
+ private defaultLimit: number;
17
+ private defaultTtl: number;
18
+
19
+ constructor(@Inject(THROTTLER_OPTIONS) private options: ThrottlerModuleOptions) {
20
+ this.storage = options.storage || new ThrottlerInMemoryStorage();
21
+ this.defaultLimit = options.limit || 10;
22
+ this.defaultTtl = options.ttl || 60;
23
+ }
24
+
25
+ getGlobalOptions() {
26
+ return { limit: this.defaultLimit, ttl: this.defaultTtl };
27
+ }
28
+
29
+ async handleRequest(c: Context, limit: number, ttl: number): Promise<boolean> {
30
+ const ip = c.req.header("x-forwarded-for") || "unknown";
31
+ const path = c.req.path;
32
+ const key = `throttler:${ip}:${path}`;
33
+
34
+ const { total, timeToExpire } = await this.storage.increment(key, ttl);
35
+
36
+ // Headers
37
+ c.header("X-RateLimit-Limit", limit.toString());
38
+ c.header("X-RateLimit-Remaining", Math.max(0, limit - total).toString());
39
+ c.header("X-RateLimit-Reset", timeToExpire.toString());
40
+
41
+ if (total > limit) {
42
+ c.status(429);
43
+ c.header("Retry-After", timeToExpire.toString());
44
+ c.text(`Too Many Requests`); // Body
45
+ return false; // Blocks request
46
+ }
47
+
48
+ return true; // Allowed
49
+ }
50
+ }
@@ -0,0 +1,22 @@
1
+ export interface ThrottlerStorage {
2
+ increment(key: string, ttl: number): Promise<{ total: number; timeToExpire: number }>;
3
+ }
4
+
5
+ export class ThrottlerInMemoryStorage implements ThrottlerStorage {
6
+ private storage = new Map<string, { total: number; expiresAt: number }>();
7
+
8
+ async increment(key: string, ttl: number): Promise<{ total: number; timeToExpire: number }> {
9
+ const now = Date.now();
10
+ const record = this.storage.get(key);
11
+
12
+ if (!record || record.expiresAt < now) {
13
+ const expiresAt = now + ttl * 1000;
14
+ this.storage.set(key, { total: 1, expiresAt });
15
+ return { total: 1, timeToExpire: ttl };
16
+ }
17
+
18
+ record.total++;
19
+ const timeToExpire = Math.ceil((record.expiresAt - now) / 1000);
20
+ return { total: record.total, timeToExpire: Math.max(0, timeToExpire) };
21
+ }
22
+ }