@wxn0brp/falcon-frame-plugin 0.0.1 → 0.0.3
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 +106 -0
- package/dist/plugins/rateLimit.d.ts +69 -2
- package/dist/plugins/rateLimit.js +74 -10
- package/package.json +37 -40
- package/dist/test.d.ts +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# FalconFrame Plugin System
|
|
2
|
+
|
|
3
|
+
A flexible plugin system for the **FalconFrame** framework, allowing developers to register, sort, and execute plugins in a specific order based on dependencies.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The **FalconFrame Plugin System** provides a robust way to manage plugins for FalconFrame applications. It supports dependency management using `before` and `after` constraints, ensuring that plugins are executed in the correct order. The system uses a **topological sort** algorithm to handle complex dependency chains and detect circular dependencies.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @wxn0brp/falcon-frame-plugin
|
|
13
|
+
# and FalconFrame if not already installed
|
|
14
|
+
npm install @wxn0brp/falcon-frame
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
* **Plugin Registration** – Register plugins with optional dependency constraints.
|
|
20
|
+
* **Dependency Management** – Specify `before` and `after` relationships between plugins.
|
|
21
|
+
* **Automatic Sorting** – Plugins are automatically ordered based on dependencies.
|
|
22
|
+
* **Circular Dependency Detection** – Throws errors when circular dependencies are detected.
|
|
23
|
+
* **Route Handler Integration** – Seamless integration with FalconFrame's routing system.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Basic Example
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { PluginSystem } from "@wxn0brp/falcon-frame-plugin";
|
|
31
|
+
import { FalconFrame } from "@wxn0brp/falcon-frame";
|
|
32
|
+
|
|
33
|
+
const app = new FalconFrame();
|
|
34
|
+
const pluginSystem = new PluginSystem();
|
|
35
|
+
|
|
36
|
+
// Register a plugin
|
|
37
|
+
pluginSystem.register({
|
|
38
|
+
id: "my-plugin",
|
|
39
|
+
process: (req, res, next) => {
|
|
40
|
+
console.log("Executing my plugin");
|
|
41
|
+
next();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Register multiple plugins
|
|
46
|
+
app.use(pluginSystem);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Plugin with Dependencies
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
pluginSystem.register({
|
|
53
|
+
id: "auth-plugin",
|
|
54
|
+
process: (req, res, next) => {
|
|
55
|
+
// Authentication logic
|
|
56
|
+
next();
|
|
57
|
+
},
|
|
58
|
+
before: "data-plugin" // Runs before "data-plugin"
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pluginSystem.register({
|
|
62
|
+
id: "data-plugin",
|
|
63
|
+
process: (req, res, next) => {
|
|
64
|
+
// Data processing logic
|
|
65
|
+
next();
|
|
66
|
+
},
|
|
67
|
+
after: "auth-plugin" // Runs after "auth-plugin"
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Plugin Interface
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface Plugin {
|
|
75
|
+
id: string; // Unique identifier for the plugin
|
|
76
|
+
process: RouteHandler; // Function executed when the plugin runs
|
|
77
|
+
before?: string | string[]; // Plugin(s) that should run after this one
|
|
78
|
+
after?: string | string[]; // Plugin(s) that should run before this one
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
### `PluginSystem.register(plugin: Plugin, opts?: PSOpts)`
|
|
85
|
+
|
|
86
|
+
Registers a plugin with the system. If `opts.after` or `opts.before` are provided, they will define the execution order. Plugins are re-sorted automatically after registration.
|
|
87
|
+
|
|
88
|
+
### `PluginSystem.getRouteHandler(): RouteHandler`
|
|
89
|
+
|
|
90
|
+
Returns a **RouteHandler** that executes all registered plugins in the correct order. It will sort the plugins if they haven’t been sorted yet, then execute them recursively.
|
|
91
|
+
|
|
92
|
+
## Sorting Algorithm
|
|
93
|
+
|
|
94
|
+
The system uses a **topological sort** to:
|
|
95
|
+
|
|
96
|
+
* Detect circular dependencies and throw errors
|
|
97
|
+
* Ensure correct execution order based on dependencies
|
|
98
|
+
* Prevent duplicate plugin IDs
|
|
99
|
+
|
|
100
|
+
## Contributing
|
|
101
|
+
|
|
102
|
+
Contributions are welcome! Please open an issue or pull request for bug reports, feature requests, or improvements.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License – see the [LICENSE](LICENSE) file for details.
|
|
@@ -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
|
-
|
|
5
|
+
windowStart: number;
|
|
5
6
|
}
|
|
6
|
-
export
|
|
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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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(
|
|
10
|
-
if (!record
|
|
11
|
-
rateLimitMap.set(
|
|
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
|
-
|
|
20
|
-
rateLimitMap.set(ip, record);
|
|
84
|
+
rateLimitMap.set(id, record);
|
|
21
85
|
next();
|
|
22
86
|
},
|
|
23
87
|
};
|
package/package.json
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"default": "./dist/*.js"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
2
|
+
"name": "@wxn0brp/falcon-frame-plugin",
|
|
3
|
+
"version": "0.0.3",
|
|
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
|
+
"devDependencies": {
|
|
16
|
+
"@types/bun": "*",
|
|
17
|
+
"@wxn0brp/falcon-frame": "^0.5.0",
|
|
18
|
+
"tsc-alias": "*",
|
|
19
|
+
"typescript": "*"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@wxn0brp/falcon-frame": ">=0.5.0"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./*": {
|
|
34
|
+
"types": "./dist/*.d.ts",
|
|
35
|
+
"import": "./dist/*.js",
|
|
36
|
+
"default": "./dist/*.js"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
42
39
|
}
|
package/dist/test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|