@wxn0brp/falcon-frame-plugin 0.0.2 → 0.0.4

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 wxn0brP
3
+ Copyright (c) 2026 wxn0brP
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,73 @@
1
+ import { FFRequest } from "@wxn0brp/falcon-frame";
1
2
  import { Plugin } from "../types.js";
2
3
  export interface RateLimitRecord {
3
4
  count: number;
4
- lastRequest: number;
5
+ windowStart: number;
5
6
  }
6
- export declare function createRateLimiterPlugin(maxRequests: number, windowMs: number): Plugin;
7
+ export interface RateLimiterContext {
8
+ id: string;
9
+ retryAfter: number;
10
+ remainingRequests: number;
11
+ record: RateLimitRecord;
12
+ }
13
+ export interface RateLimiterOptions {
14
+ /**
15
+ * Maximum number of requests allowed per window.
16
+ */
17
+ maxRequests: number;
18
+ /**
19
+ * Window duration in milliseconds.
20
+ */
21
+ windowMs: number;
22
+ /**
23
+ * Callback triggered when the rate limit is exceeded.
24
+ * Gives full control over the response.
25
+ *
26
+ * @param req - HTTP request object.
27
+ * @param res - HTTP response object.
28
+ * @param ctx - Context object containing limit info (IP, retryAfter, remainingRequests, etc.).
29
+ */
30
+ onLimitReached?: (req: any, res: any, ctx: RateLimiterContext) => void;
31
+ /**
32
+ * Disable the automatic cleanup interval.
33
+ * Default: false.
34
+ */
35
+ disableCleanup?: boolean;
36
+ /**
37
+ * Shared Map instance to use instead of a private one.
38
+ * Useful if multiple limiters need to share state.
39
+ */
40
+ sharedMap?: Map<string, RateLimitRecord>;
41
+ /**
42
+ * Function to generate a unique key for each request.
43
+ * Default: uses the IP address of the request.
44
+ */
45
+ id?: (req: FFRequest) => string | Promise<string>;
46
+ }
47
+ /**
48
+ * Creates a rate limiter plugin for HTTP requests based on IP.
49
+ *
50
+ * Uses a simple "fixed window counter" algorithm and can optionally
51
+ * share the internal Map or disable cleanup interval.
52
+ *
53
+ * @example
54
+ * const sharedMap = new Map();
55
+ * const rateLimiter = createRateLimiterPlugin({
56
+ * maxRequests: 5,
57
+ * windowMs: 60_000,
58
+ * sharedMap,
59
+ * onLimitReached: (req, res, ctx) => {
60
+ * res.statusCode = 429;
61
+ * res.json({
62
+ * message: "Sorry, too many requests!",
63
+ * retryAfter: ctx.retryAfter,
64
+ * remaining: ctx.remainingRequests,
65
+ * ip: ctx.ip,
66
+ * });
67
+ * },
68
+ * });
69
+ *
70
+ * @param {RateLimiterOptions} options - Rate limiter configuration.
71
+ * @returns {Plugin} Middleware plugin compatible with your system.
72
+ */
73
+ export declare function createRateLimiterPlugin(opts: RateLimiterOptions): Plugin;
@@ -1,23 +1,87 @@
1
- ;
2
- export function createRateLimiterPlugin(maxRequests, windowMs) {
3
- const rateLimitMap = new Map();
1
+ /**
2
+ * Creates a rate limiter plugin for HTTP requests based on IP.
3
+ *
4
+ * Uses a simple "fixed window counter" algorithm and can optionally
5
+ * share the internal Map or disable cleanup interval.
6
+ *
7
+ * @example
8
+ * const sharedMap = new Map();
9
+ * const rateLimiter = createRateLimiterPlugin({
10
+ * maxRequests: 5,
11
+ * windowMs: 60_000,
12
+ * sharedMap,
13
+ * onLimitReached: (req, res, ctx) => {
14
+ * res.statusCode = 429;
15
+ * res.json({
16
+ * message: "Sorry, too many requests!",
17
+ * retryAfter: ctx.retryAfter,
18
+ * remaining: ctx.remainingRequests,
19
+ * ip: ctx.ip,
20
+ * });
21
+ * },
22
+ * });
23
+ *
24
+ * @param {RateLimiterOptions} options - Rate limiter configuration.
25
+ * @returns {Plugin} Middleware plugin compatible with your system.
26
+ */
27
+ export function createRateLimiterPlugin(opts) {
28
+ const { maxRequests, windowMs, onLimitReached, disableCleanup = false, sharedMap, } = opts;
29
+ const rateLimitMap = sharedMap ?? new Map();
30
+ // Optional automatic cleanup
31
+ if (!disableCleanup) {
32
+ const cleanupInterval = setInterval(() => {
33
+ const now = Date.now();
34
+ for (const [id, record] of rateLimitMap.entries()) {
35
+ if (now - record.windowStart > windowMs) {
36
+ rateLimitMap.delete(id);
37
+ }
38
+ }
39
+ }, windowMs * 2);
40
+ cleanupInterval.unref?.();
41
+ }
4
42
  return {
5
43
  id: "rateLimiter",
6
- process: (req, res, next) => {
7
- const ip = req.socket.remoteAddress ?? "unknown";
44
+ process: async (req, res, next) => {
45
+ let id;
46
+ if (opts.id) {
47
+ id = await opts.id(req);
48
+ }
49
+ else {
50
+ id = req.socket.remoteAddress ?? "unknown";
51
+ }
8
52
  const now = Date.now();
9
- const record = rateLimitMap.get(ip);
10
- if (!record || now - record.lastRequest > windowMs) {
11
- rateLimitMap.set(ip, { count: 1, lastRequest: now });
53
+ const record = rateLimitMap.get(id);
54
+ if (!record) {
55
+ rateLimitMap.set(id, { count: 1, windowStart: now });
56
+ return next();
57
+ }
58
+ const elapsed = now - record.windowStart;
59
+ // Reset window if it expired
60
+ if (elapsed > windowMs) {
61
+ rateLimitMap.set(id, { count: 1, windowStart: now });
12
62
  return next();
13
63
  }
64
+ // Rate limit exceeded
14
65
  if (record.count >= maxRequests) {
66
+ const retryAfter = Math.ceil((windowMs - elapsed) / 1000);
67
+ const remainingRequests = Math.max(0, maxRequests - record.count);
15
68
  res.statusCode = 429;
69
+ res.setHeader("Retry-After", retryAfter);
70
+ const ctx = {
71
+ id,
72
+ retryAfter,
73
+ remainingRequests,
74
+ record,
75
+ };
76
+ if (onLimitReached)
77
+ return onLimitReached(req, res, ctx);
78
+ // Default plain text response
79
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
16
80
  return res.end("Too Many Requests");
17
81
  }
82
+ // Increment count
18
83
  record.count++;
19
- record.lastRequest = now;
20
- rateLimitMap.set(ip, record);
84
+ rateLimitMap.set(id, record);
21
85
  next();
22
86
  },
23
87
  };
package/package.json CHANGED
@@ -1,42 +1,43 @@
1
1
  {
2
- "name": "@wxn0brp/falcon-frame-plugin",
3
- "version": "0.0.2",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "description": "",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/wxn0brP/FalconFrame-plugin.git"
10
- },
11
- "homepage": "https://github.com/wxn0brP/FalconFrame-plugin",
12
- "author": "wxn0brP",
13
- "license": "MIT",
14
- "type": "module",
15
- "scripts": {
16
- "build": "tsc && tsc-alias"
17
- },
18
- "devDependencies": {
19
- "@types/bun": "*",
20
- "@wxn0brp/falcon-frame": "^0.5.0",
21
- "tsc-alias": "*",
22
- "typescript": "*"
23
- },
24
- "peerDependencies": {
25
- "@wxn0brp/falcon-frame": ">=0.5.0"
26
- },
27
- "files": [
28
- "dist"
29
- ],
30
- "exports": {
31
- ".": {
32
- "types": "./dist/index.d.ts",
33
- "import": "./dist/index.js",
34
- "default": "./dist/index.js"
35
- },
36
- "./*": {
37
- "types": "./dist/*.d.ts",
38
- "import": "./dist/*.js",
39
- "default": "./dist/*.js"
40
- }
41
- }
42
- }
2
+ "name": "@wxn0brp/falcon-frame-plugin",
3
+ "version": "0.0.4",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "description": "Plugin system for FalconFrame",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/wxn0brP/FalconFrame-plugin.git"
10
+ },
11
+ "homepage": "https://github.com/wxn0brP/FalconFrame-plugin",
12
+ "author": "wxn0brP",
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "keywords": [
16
+ "falcon-frame",
17
+ "plugin"
18
+ ],
19
+ "devDependencies": {
20
+ "@types/bun": "*",
21
+ "@wxn0brp/falcon-frame": "^0.7.0",
22
+ "tsc-alias": "*",
23
+ "typescript": "*"
24
+ },
25
+ "peerDependencies": {
26
+ "@wxn0brp/falcon-frame": ">=0.7.0"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "./*": {
38
+ "types": "./dist/*.d.ts",
39
+ "import": "./dist/*.js",
40
+ "default": "./dist/*.js"
41
+ }
42
+ }
43
+ }
package/dist/test.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};