keypointjs 1.0.0 → 1.1.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 +16 -6
- package/package.json +2 -1
- package/src/core/ProtocolEngine.js +216 -63
- package/src/keypointJS.js +254 -168
- package/src/plugins/transformer/TransformerPlugin.js +82 -0
package/README.md
CHANGED
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
Based on your complete codebase, here is the comprehensive documentation:
|
|
10
|
-
|
|
11
9
|
<div align="center">
|
|
12
10
|
<p align="center">
|
|
13
11
|
<img alt="GitHub" src="https://img.shields.io/github/license/anasbex-dev/keypointjs?color=blue">
|
|
14
12
|
<img alt="npm" src="https://img.shields.io/npm/v/keypointjs">
|
|
13
|
+
<img alt="npmcharts" src="https://img.shields.io/npm/dm/keypointjs?style=for-the-badge">
|
|
15
14
|
<img alt="Node.js" src="https://img.shields.io/badge/Node.js-%3E%3D18.0.0-green">
|
|
16
15
|
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-Ready-blue">
|
|
17
16
|
<img alt="Tests" src="https://img.shields.io/badge/tests-100%25%20passing-brightgreen">
|
|
@@ -23,7 +22,6 @@ Based on your complete codebase, here is the comprehensive documentation:
|
|
|
23
22
|
</div>
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
|
|
27
25
|
# Project Overview
|
|
28
26
|
|
|
29
27
|
KeypointJS is a sophisticated, layered authentication and authorization framework for Node.js with built-in security features, plugin architecture, and real-time capabilities.
|
|
@@ -51,7 +49,6 @@ Layered Middleware System
|
|
|
51
49
|
│ Layer 7: Response Processing │
|
|
52
50
|
└─────────────────────────────────┘
|
|
53
51
|
```
|
|
54
|
-
|
|
55
52
|
# File Structure & Responsibilities
|
|
56
53
|
|
|
57
54
|
## Core Components (core/)
|
|
@@ -192,6 +189,16 @@ responses
|
|
|
192
189
|
|
|
193
190
|
Installation & Setup
|
|
194
191
|
|
|
192
|
+
``` bash
|
|
193
|
+
|
|
194
|
+
npm install keypointjs
|
|
195
|
+
# or
|
|
196
|
+
yarn add keypointjs
|
|
197
|
+
# or
|
|
198
|
+
pnpm add keypointjs
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
|
|
195
202
|
```javascript
|
|
196
203
|
import { KeypointJS } from './src/keypointJS.js';
|
|
197
204
|
|
|
@@ -800,9 +807,12 @@ Apache-2.0 license - see the LICENSE file for details.
|
|
|
800
807
|
- Contributions: PRs welcome for bug fixes and features
|
|
801
808
|
- Questions: Open a discussion for usage questions
|
|
802
809
|
|
|
803
|
-
|
|
810
|
+
## KeypointJS is Independent
|
|
811
|
+
|
|
812
|
+
KeypointJS does not depend on Express, Fastify, or any third-party HTTP framework.
|
|
813
|
+
It ships with its own HTTP server, routing system, middleware pipeline, and security layer.
|
|
804
814
|
|
|
805
815
|
## Created Base ♥️ KeypointJS
|
|
806
816
|
### AnasBex - (づ ̄ ³ ̄)づ
|
|
807
817
|
|
|
808
|
-
KeypointJS provides a comprehensive, layered approach to API security with extensibility through plugins, real-time capabilities via WebSocket, and detailed monitoring through audit logging. The framework is production-ready with built-in security features and can be extended to meet specific requirements.
|
|
818
|
+
KeypointJS provides a comprehensive, layered approach to API security with extensibility through plugins, real-time capabilities via WebSocket, and detailed monitoring through audit logging. The framework is production-ready with built-in security features and can be extended to meet specific requirements.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keypointjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "KeypointJS Identity-First API Framework with Mandatory Authentication",
|
|
5
5
|
"main": "src/keypointJS.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"./keypoint/validator": "./src/keypoint/KeypointValidator.js",
|
|
13
13
|
"./keypoint/scopes": "./src/keypoint/ScopeManager.js",
|
|
14
14
|
"./plugins": "./src/plugins/PluginManager.js",
|
|
15
|
+
"./plugins/transformer": "./src/plugins/transformer/TransformerPlugin.js",
|
|
15
16
|
"./plugins/audit": "./src/plugins/AuditLogger.js",
|
|
16
17
|
"./plugins/ratelimit": "./src/plugins/RateLimiter.js",
|
|
17
18
|
"./plugins/websocket": "./src/plugins/WebSocketGuard.js",
|
|
@@ -6,52 +6,106 @@ export class ProtocolEngine {
|
|
|
6
6
|
maxBodySize: '1mb',
|
|
7
7
|
parseJSON: true,
|
|
8
8
|
parseForm: true,
|
|
9
|
+
validateProtocol: true, // protocol validation
|
|
10
|
+
trustedProxies: [], // trusted proxy list
|
|
11
|
+
maxHeaderSize: 8192, // header limits
|
|
9
12
|
...options
|
|
10
13
|
};
|
|
11
14
|
|
|
12
|
-
this.supportedProtocols = new Set(['https', 'wss', 'http']);
|
|
15
|
+
this.supportedProtocols = new Set(['https', 'wss', 'http', 'grpc']); // grpc
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
async process(request) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
// Validate request basics
|
|
20
|
+
if (!request.method || !request.url) {
|
|
21
|
+
throw new ProtocolError('Invalid request structure', 400);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate headers size
|
|
25
|
+
const headerSize = JSON.stringify(request.headers).length;
|
|
26
|
+
if (headerSize > this.options.maxHeaderSize) {
|
|
27
|
+
throw new ProtocolError('Request headers too large', 431);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const context = {
|
|
31
|
+
id: crypto.randomUUID(),
|
|
32
|
+
timestamp: new Date(),
|
|
33
|
+
protocol: this.detectProtocol(request),
|
|
34
|
+
request: {
|
|
35
|
+
method: request.method,
|
|
36
|
+
url: this.parseURL(request),
|
|
37
|
+
headers: this.normalizeHeaders(request.headers),
|
|
38
|
+
ip: this.extractIP(request),
|
|
39
|
+
userAgent: request.headers['user-agent'] || '',
|
|
40
|
+
body: null,
|
|
41
|
+
cookies: this.parseCookies(request.headers.cookie), // cookies
|
|
42
|
+
trailers: request.trailers || {} // trailers
|
|
43
|
+
},
|
|
44
|
+
metadata: {
|
|
45
|
+
bodyParsed: false,
|
|
46
|
+
secure: false,
|
|
47
|
+
clientInfo: {}
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Set secure flag
|
|
52
|
+
context.metadata.secure = context.protocol === 'https' || context.protocol === 'wss';
|
|
53
|
+
|
|
54
|
+
// Add client information
|
|
55
|
+
context.metadata.clientInfo = this.extractClientInfo(request);
|
|
56
|
+
|
|
57
|
+
// Parse body if needed
|
|
58
|
+
if (this.shouldParseBody(request)) {
|
|
59
|
+
context.request.body = await this.parseRequestBody(request);
|
|
60
|
+
context.metadata.bodyParsed = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate protocol
|
|
64
|
+
if (this.options.validateProtocol && !this.supportedProtocols.has(context.protocol)) {
|
|
65
|
+
throw new ProtocolError(
|
|
66
|
+
`Unsupported protocol: ${context.protocol}. Supported: ${Array.from(this.supportedProtocols).join(', ')}`,
|
|
67
|
+
426 // Upgrade Required
|
|
68
|
+
);
|
|
42
69
|
}
|
|
43
70
|
|
|
71
|
+
return context;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
detectProtocol(request) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
// Prioritize headers
|
|
76
|
+
const forwardedProto = request.headers['x-forwarded-proto'];
|
|
77
|
+
if (forwardedProto) {
|
|
78
|
+
// Security: Validate against trusted proxies
|
|
79
|
+
const clientIP = this.extractIP(request);
|
|
80
|
+
if (this.isTrustedProxy(clientIP)) {
|
|
81
|
+
return forwardedProto.split(',')[0].trim();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for ALPN protocol (HTTP/2, HTTP/3)
|
|
86
|
+
const alpnProtocol = request.socket?.alpnProtocol;
|
|
87
|
+
if (alpnProtocol) {
|
|
88
|
+
if (alpnProtocol === 'h2') return 'https';
|
|
89
|
+
if (alpnProtocol === 'h3') return 'https';
|
|
53
90
|
}
|
|
54
91
|
|
|
92
|
+
// Standard detection
|
|
93
|
+
const isSecure = request.connection?.encrypted ||
|
|
94
|
+
request.socket?.encrypted ||
|
|
95
|
+
request.secure ||
|
|
96
|
+
(request.headers['x-arr-ssl'] !== undefined) ||
|
|
97
|
+
(request.headers['x-forwarded-ssl'] === 'on');
|
|
98
|
+
|
|
99
|
+
return isSecure ? 'https' : 'http';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isTrustedProxy(ip) {
|
|
103
|
+
if (this.options.trustedProxies.length === 0) return true;
|
|
104
|
+
return this.options.trustedProxies.some(proxy =>
|
|
105
|
+
proxy === ip || this.isIPInCIDR(ip, proxy)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
normalizeHeaders(headers) {
|
|
56
110
|
const normalized = {};
|
|
57
111
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -79,42 +133,128 @@ export class ProtocolEngine {
|
|
|
79
133
|
}
|
|
80
134
|
|
|
81
135
|
async parseRequestBody(request) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
let body = Buffer.from('');
|
|
138
|
+
let totalSize = 0;
|
|
139
|
+
|
|
140
|
+
request.on('data', chunk => {
|
|
141
|
+
totalSize += chunk.length;
|
|
142
|
+
|
|
143
|
+
// Check size limit early
|
|
144
|
+
if (totalSize > this.parseSizeLimit()) {
|
|
145
|
+
request.destroy();
|
|
146
|
+
reject(new ProtocolError(`Request body too large (max: ${this.options.maxBodySize})`, 413));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
body = Buffer.concat([body, chunk]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
request.on('end', async () => {
|
|
154
|
+
try {
|
|
155
|
+
const contentType = request.headers['content-type'] || '';
|
|
156
|
+
const mimeType = contentType.split(';')[0].trim();
|
|
86
157
|
|
|
87
|
-
//
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
158
|
+
// Stream parsing for large files
|
|
159
|
+
if (totalSize > 1024 * 1024) { // > 1MB
|
|
160
|
+
resolve(await this.parseLargeBody(body, mimeType));
|
|
161
|
+
return;
|
|
91
162
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
|
|
164
|
+
const bodyString = body.toString('utf-8');
|
|
165
|
+
|
|
166
|
+
// Content-Type specific parsing
|
|
167
|
+
if (mimeType === 'application/json' && this.options.parseJSON) {
|
|
168
|
+
if (bodyString.trim() === '') resolve({});
|
|
169
|
+
else resolve(JSON.parse(bodyString));
|
|
170
|
+
}
|
|
171
|
+
else if (mimeType === 'application/x-www-form-urlencoded' && this.options.parseForm) {
|
|
172
|
+
const params = new URLSearchParams(bodyString);
|
|
173
|
+
const result = {};
|
|
174
|
+
for (const [key, value] of params) {
|
|
175
|
+
// Handle duplicate keys (array values)
|
|
176
|
+
if (key in result) {
|
|
177
|
+
if (Array.isArray(result[key])) {
|
|
178
|
+
result[key].push(value);
|
|
179
|
+
} else {
|
|
180
|
+
result[key] = [result[key], value];
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
104
183
|
result[key] = value;
|
|
105
184
|
}
|
|
106
|
-
resolve(result);
|
|
107
|
-
} else {
|
|
108
|
-
resolve(body);
|
|
109
185
|
}
|
|
110
|
-
|
|
111
|
-
reject(new ProtocolError(`Failed to parse body: ${error.message}`));
|
|
186
|
+
resolve(result);
|
|
112
187
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
188
|
+
else if (mimeType === 'multipart/form-data') {
|
|
189
|
+
resolve(await this.parseMultipartFormData(body, contentType));
|
|
190
|
+
}
|
|
191
|
+
else if (mimeType === 'text/plain' || mimeType === 'text/html') {
|
|
192
|
+
resolve(bodyString);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Raw buffer untuk binary data
|
|
196
|
+
resolve(body);
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
reject(new ProtocolError(
|
|
200
|
+
`Failed to parse body: ${error.message}`,
|
|
201
|
+
400,
|
|
202
|
+
{ contentType: request.headers['content-type'] }
|
|
203
|
+
));
|
|
204
|
+
}
|
|
116
205
|
});
|
|
206
|
+
|
|
207
|
+
request.on('error', reject);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
parseURL(request) {
|
|
212
|
+
try {
|
|
213
|
+
const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
|
|
214
|
+
|
|
215
|
+
// Security: Sanitize URL
|
|
216
|
+
return {
|
|
217
|
+
href: url.href,
|
|
218
|
+
origin: url.origin,
|
|
219
|
+
protocol: url.protocol,
|
|
220
|
+
hostname: url.hostname,
|
|
221
|
+
port: url.port,
|
|
222
|
+
pathname: url.pathname,
|
|
223
|
+
search: url.search,
|
|
224
|
+
searchParams: url.searchParams,
|
|
225
|
+
hash: url.hash,
|
|
226
|
+
toString: () => url.toString()
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw new ProtocolError(`Invalid URL: ${request.url}`, 400);
|
|
117
230
|
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
extractClientInfo(request) {
|
|
234
|
+
return {
|
|
235
|
+
ip: this.extractIP(request),
|
|
236
|
+
port: request.socket?.remotePort,
|
|
237
|
+
family: request.socket?.remoteFamily,
|
|
238
|
+
tlsVersion: request.socket?.getProtocol?.(),
|
|
239
|
+
cipher: request.socket?.getCipher?.(),
|
|
240
|
+
servername: request.socket?.servername, // SNI
|
|
241
|
+
authenticated: request.socket?.authorized || false,
|
|
242
|
+
certificate: request.socket?.getPeerCertificate?.() || null
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
parseCookies(cookieHeader) {
|
|
247
|
+
if (!cookieHeader) return {};
|
|
248
|
+
|
|
249
|
+
const cookies = {};
|
|
250
|
+
cookieHeader.split(';').forEach(cookie => {
|
|
251
|
+
const [name, ...valueParts] = cookie.trim().split('=');
|
|
252
|
+
if (name) {
|
|
253
|
+
cookies[name] = decodeURIComponent(valueParts.join('=') || '');
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return cookies;
|
|
257
|
+
}
|
|
118
258
|
|
|
119
259
|
parseSizeLimit() {
|
|
120
260
|
const size = this.options.maxBodySize;
|
|
@@ -135,10 +275,23 @@ export class ProtocolEngine {
|
|
|
135
275
|
}
|
|
136
276
|
|
|
137
277
|
export class ProtocolError extends Error {
|
|
138
|
-
constructor(message, code = 400) {
|
|
278
|
+
constructor(message, code = 400, details = {}) {
|
|
139
279
|
super(message);
|
|
140
280
|
this.name = 'ProtocolError';
|
|
141
281
|
this.code = code;
|
|
282
|
+
this.details = details;
|
|
142
283
|
this.timestamp = new Date();
|
|
284
|
+
this.stackTrace = process.env.NODE_ENV === 'development' ? this.stack : undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
toJSON() {
|
|
288
|
+
return {
|
|
289
|
+
name: this.name,
|
|
290
|
+
message: this.message,
|
|
291
|
+
code: this.code,
|
|
292
|
+
timestamp: this.timestamp.toISOString(),
|
|
293
|
+
details: this.details,
|
|
294
|
+
...(process.env.NODE_ENV === 'development' && { stack: this.stackTrace })
|
|
295
|
+
};
|
|
143
296
|
}
|
|
144
297
|
}
|
package/src/keypointJS.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
console.error('Failed to import http module:', error);
|
|
7
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
KeypointJS Main Module © 2026
|
|
3
|
+
__________________________________________
|
|
4
|
+
|
|
5
|
+
*/
|
|
8
6
|
|
|
9
7
|
import { Context } from './core/Context.js';
|
|
10
8
|
import { ProtocolEngine, ProtocolError } from './core/ProtocolEngine.js';
|
|
@@ -20,6 +18,7 @@ import { PluginManager, BuiltInHooks } from './plugins/PluginManager.js';
|
|
|
20
18
|
import { RateLimiter } from './plugins/RateLimiter.js';
|
|
21
19
|
import { AuditLogger } from './plugins/AuditLogger.js';
|
|
22
20
|
import { WebSocketGuard } from './plugins/WebSocketGuard.js';
|
|
21
|
+
import { AccessDecision } from './policy/AccessDecision.js';
|
|
23
22
|
|
|
24
23
|
export class KeypointJS {
|
|
25
24
|
constructor(options = {}) {
|
|
@@ -36,6 +35,7 @@ export class KeypointJS {
|
|
|
36
35
|
'X-Content-Type-Options': 'nosniff'
|
|
37
36
|
},
|
|
38
37
|
errorHandler: this.defaultErrorHandler.bind(this),
|
|
38
|
+
trustedProxies: [], // TAMBAH: untuk ProtocolEngine
|
|
39
39
|
...options
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -48,6 +48,9 @@ export class KeypointJS {
|
|
|
48
48
|
// Setup built-in policies
|
|
49
49
|
this.setupBuiltInPolicies();
|
|
50
50
|
|
|
51
|
+
// Setup built-in plugins
|
|
52
|
+
this.setupBuiltInPlugins(); // TAMBAH METHOD BARU
|
|
53
|
+
|
|
51
54
|
// Event emitter
|
|
52
55
|
this.events = new Map();
|
|
53
56
|
|
|
@@ -62,6 +65,38 @@ export class KeypointJS {
|
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
getProtocolEngine() {
|
|
69
|
+
return this.protocolEngine;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
configureProtocolEngine(options) {
|
|
73
|
+
this.protocolEngine = new ProtocolEngine({
|
|
74
|
+
...this.protocolEngine.options,
|
|
75
|
+
...options
|
|
76
|
+
});
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setupBuiltInPlugins() {
|
|
81
|
+
// Register built-in plugins jika di-enable via options
|
|
82
|
+
if (this.options.enableAuditLog !== false) {
|
|
83
|
+
const auditLogger = new AuditLogger({
|
|
84
|
+
logToConsole: this.options.auditToConsole !== false,
|
|
85
|
+
logToFile: this.options.auditToFile || false,
|
|
86
|
+
filePath: this.options.auditFilePath || './audit.log'
|
|
87
|
+
});
|
|
88
|
+
this.registerPlugin(auditLogger);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.options.enableRateLimiter !== false) {
|
|
92
|
+
const rateLimiter = new RateLimiter({
|
|
93
|
+
window: this.options.rateLimitWindow || 60000,
|
|
94
|
+
max: this.options.rateLimitMax || 100
|
|
95
|
+
});
|
|
96
|
+
this.registerPlugin(rateLimiter);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
initializeCore() {
|
|
66
101
|
// Core protocol engine
|
|
67
102
|
this.protocolEngine = new ProtocolEngine({
|
|
@@ -101,25 +136,45 @@ export class KeypointJS {
|
|
|
101
136
|
// Layer 1: Protocol Engine
|
|
102
137
|
this.use(async (ctx, next) => {
|
|
103
138
|
try {
|
|
139
|
+
// Make sure ctx.request is a native Node.js Request object.
|
|
140
|
+
if (!ctx.request || typeof ctx.request !== 'object') {
|
|
141
|
+
throw new ProtocolError('Invalid request object', 400);
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
const processed = await this.protocolEngine.process(ctx.request);
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
ctx
|
|
108
|
-
|
|
146
|
+
// Update context dengan data processed
|
|
147
|
+
Object.assign(ctx, {
|
|
148
|
+
id: processed.id,
|
|
149
|
+
timestamp: processed.timestamp,
|
|
150
|
+
metadata: {
|
|
151
|
+
...ctx.metadata,
|
|
152
|
+
...processed.metadata
|
|
153
|
+
}
|
|
154
|
+
});
|
|
109
155
|
|
|
110
|
-
// Update request object
|
|
111
|
-
|
|
112
|
-
ctx.request
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
156
|
+
// Update request object - pertahankan original request
|
|
157
|
+
ctx.request = {
|
|
158
|
+
...ctx.request,
|
|
159
|
+
...processed.request,
|
|
160
|
+
originalRequest: ctx.request // Save reference to original
|
|
161
|
+
};
|
|
117
162
|
|
|
163
|
+
// Set protocol and IP
|
|
118
164
|
ctx.setState('_protocol', processed.protocol);
|
|
119
|
-
ctx.setState('_ip', processed.request?.ip);
|
|
165
|
+
ctx.setState('_ip', processed.request?.ip || '0.0.0.0');
|
|
166
|
+
|
|
167
|
+
// Set protocol and ip properties in context
|
|
168
|
+
if (!ctx._protocol) {
|
|
169
|
+
ctx._protocol = processed.protocol;
|
|
170
|
+
}
|
|
120
171
|
|
|
121
172
|
} catch (error) {
|
|
122
|
-
|
|
173
|
+
// Use ProtocolError if available, otherwise KeypointError
|
|
174
|
+
if (error.name === 'ProtocolError') {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
throw new ProtocolError(`Protocol processing error: ${error.message}`, 400);
|
|
123
178
|
}
|
|
124
179
|
return next(ctx);
|
|
125
180
|
});
|
|
@@ -410,32 +465,42 @@ this.use(async (ctx, next) => {
|
|
|
410
465
|
// Request handling
|
|
411
466
|
|
|
412
467
|
async handleRequest(request, response) {
|
|
413
|
-
|
|
414
|
-
|
|
468
|
+
const ctx = new KeypointContext(request);
|
|
469
|
+
this.stats.requests++;
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
// Run the middleware chain
|
|
473
|
+
await this.runMiddlewareChain(ctx);
|
|
474
|
+
this.stats.successful++;
|
|
415
475
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
476
|
+
// Emit success event
|
|
477
|
+
this.emit('request:success', {
|
|
478
|
+
ctx,
|
|
479
|
+
timestamp: new Date(),
|
|
480
|
+
duration: ctx.response?.duration || 0
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Return response
|
|
484
|
+
return ctx.response || {
|
|
485
|
+
status: 404,
|
|
486
|
+
headers: { 'Content-Type': 'application/json' },
|
|
487
|
+
body: { error: 'Not Found', code: 404 }
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
} catch (error) {
|
|
491
|
+
this.stats.failed++;
|
|
492
|
+
|
|
493
|
+
// Emit error event
|
|
494
|
+
this.emit('request:error', {
|
|
495
|
+
ctx,
|
|
496
|
+
error,
|
|
497
|
+
timestamp: new Date()
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Handle error via error handler
|
|
501
|
+
return this.options.errorHandler(error, ctx, response);
|
|
438
502
|
}
|
|
503
|
+
}
|
|
439
504
|
|
|
440
505
|
async runMiddlewareChain(ctx, index = 0) {
|
|
441
506
|
if (index >= this.middlewareChain.length) return;
|
|
@@ -446,147 +511,168 @@ this.use(async (ctx, next) => {
|
|
|
446
511
|
return await middleware(ctx, next);
|
|
447
512
|
}
|
|
448
513
|
|
|
449
|
-
// HTTP Server integration
|
|
514
|
+
// HTTP Server integration - SINGLE VERSION
|
|
450
515
|
createServer() {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
516
|
+
return new Promise(async (resolve, reject) => {
|
|
517
|
+
try {
|
|
518
|
+
// Dynamic import http module
|
|
519
|
+
const http = await import('http');
|
|
520
|
+
|
|
521
|
+
const server = http.createServer(async (req, res) => {
|
|
522
|
+
try {
|
|
523
|
+
const response = await this.handleRequest(req, res);
|
|
524
|
+
|
|
525
|
+
// Set response
|
|
526
|
+
res.statusCode = response.status || 200;
|
|
527
|
+
|
|
528
|
+
// Set headers
|
|
529
|
+
if (response.headers) {
|
|
530
|
+
Object.entries(response.headers).forEach(([key, value]) => {
|
|
531
|
+
if (value !== undefined && value !== null) {
|
|
532
|
+
res.setHeader(key, value);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Send body
|
|
538
|
+
if (response.body !== undefined && response.body !== null) {
|
|
539
|
+
const body = typeof response.body === 'string' ?
|
|
540
|
+
response.body :
|
|
541
|
+
JSON.stringify(response.body);
|
|
542
|
+
res.end(body);
|
|
543
|
+
} else {
|
|
544
|
+
res.end();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error('Server error:', error);
|
|
549
|
+
res.statusCode = 500;
|
|
550
|
+
res.setHeader('Content-Type', 'application/json');
|
|
551
|
+
res.end(JSON.stringify({
|
|
552
|
+
error: 'Internal Server Error',
|
|
553
|
+
timestamp: new Date().toISOString()
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Setup WebSocket jika ada
|
|
559
|
+
if (this.wsGuard) {
|
|
560
|
+
this.wsGuard.attachToServer(server, this);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
resolve(server);
|
|
564
|
+
|
|
565
|
+
} catch (error) {
|
|
566
|
+
reject(new Error(`Failed to create server: ${error.message}`));
|
|
470
567
|
}
|
|
471
568
|
});
|
|
472
|
-
|
|
473
|
-
if (this.wsGuard) {
|
|
474
|
-
this.wsGuard.attachToServer(server, this);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return server;
|
|
478
569
|
}
|
|
479
570
|
|
|
571
|
+
// SINGLE listen method
|
|
480
572
|
listen(port, hostname = '0.0.0.0', callback) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
573
|
+
return new Promise(async (resolve, reject) => {
|
|
574
|
+
try {
|
|
575
|
+
const server = await this.createServer();
|
|
576
|
+
|
|
577
|
+
server.listen(port, hostname, () => {
|
|
578
|
+
const address = server.address();
|
|
579
|
+
const actualHost = address.address;
|
|
580
|
+
const actualPort = address.port;
|
|
581
|
+
|
|
582
|
+
console.log(`
|
|
583
|
+
╔═══════════════════════════════════════════════╗
|
|
584
|
+
║ KeypointJS Server Started ║
|
|
585
|
+
╠═══════════════════════════════════════════════╣
|
|
586
|
+
║ Address: ${actualHost}:${actualPort}${' '.repeat(20 - (actualHost.length + actualPort.toString().length))}║
|
|
587
|
+
║ Mode: ${this.options.requireKeypoint ? 'Strict' : 'Permissive'}${' '.repeat(25 - (this.options.requireKeypoint ? 6 : 9))}║
|
|
588
|
+
║ Protocols: HTTP/HTTPS${this.wsGuard ? ' + WebSocket' : ''}${' '.repeat(25 - (this.wsGuard ? 21 : 10))}║
|
|
589
|
+
║ Plugins: ${this.pluginManager.getPluginNames().length} loaded${' '.repeat(20 - this.pluginManager.getPluginNames().length.toString().length)}║
|
|
590
|
+
║ Keypoints: ${this.keypointStorage.store.size} registered${' '.repeat(20 - this.keypointStorage.store.size.toString().length)}║
|
|
591
|
+
╚═══════════════════════════════════════════════╝
|
|
592
|
+
`);
|
|
593
|
+
|
|
594
|
+
// Emit event
|
|
595
|
+
this.emit('server:started', {
|
|
596
|
+
host: actualHost,
|
|
597
|
+
port: actualPort,
|
|
598
|
+
timestamp: new Date()
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
if (callback) callback(server);
|
|
602
|
+
resolve(server);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
server.on('error', (error) => {
|
|
606
|
+
this.emit('server:error', { error, timestamp: new Date() });
|
|
607
|
+
reject(error);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Handle graceful shutdown
|
|
611
|
+
process.on('SIGTERM', () => this.shutdown());
|
|
612
|
+
process.on('SIGINT', () => this.shutdown());
|
|
613
|
+
|
|
614
|
+
} catch (error) {
|
|
615
|
+
reject(error);
|
|
616
|
+
}
|
|
497
617
|
});
|
|
498
|
-
|
|
499
|
-
return server;
|
|
500
618
|
}
|
|
501
619
|
|
|
502
|
-
async listen(port, hostname = '0.0.0.0', callback) {
|
|
503
|
-
const server = await this.createServer();
|
|
504
|
-
|
|
505
|
-
server.listen(port, hostname, () => {
|
|
506
|
-
const address = server.address();
|
|
507
|
-
console.log(`
|
|
508
|
-
KeypointJS Server Started
|
|
509
|
-
════════════════════════════════════════
|
|
510
|
-
Address: ${hostname}:${port}
|
|
511
|
-
Mode: ${this.options.requireKeypoint ? 'Strict (Keypoint Required)' : 'Permissive'}
|
|
512
|
-
Protocols: HTTP/HTTPS${this.wsGuard ? ' + WebSocket' : ''}
|
|
513
|
-
Plugins: ${this.pluginManager.getPluginNames().length} loaded
|
|
514
|
-
Keypoints: ${this.keypointStorage.store.size} registered
|
|
515
|
-
════════════════════════════════════════
|
|
516
|
-
`);
|
|
517
|
-
|
|
518
|
-
if (callback) callback(server);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
return server;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
listen(port, hostname = '0.0.0.0', callback) {
|
|
525
|
-
const server = this.createServer();
|
|
526
|
-
|
|
527
|
-
server.listen(port, hostname, () => {
|
|
528
|
-
const address = server.address();
|
|
529
|
-
console.log(`
|
|
530
|
-
KeypointJS Server Started
|
|
531
|
-
════════════════════════════════════════
|
|
532
|
-
Address: ${hostname}:${port}
|
|
533
|
-
Mode: ${this.options.requireKeypoint ? 'Strict (Keypoint Required)' : 'Permissive'}
|
|
534
|
-
Protocols: HTTP/HTTPS${this.wsGuard ? ' + WebSocket' : ''}
|
|
535
|
-
Plugins: ${this.pluginManager.getPluginNames().length} loaded
|
|
536
|
-
Keypoints: ${this.keypointStorage.store.size} registered
|
|
537
|
-
════════════════════════════════════════
|
|
538
|
-
`);
|
|
539
|
-
|
|
540
|
-
if (callback) callback(server);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
return server;
|
|
544
|
-
}
|
|
545
620
|
|
|
546
621
|
// Error handling
|
|
547
|
-
|
|
548
622
|
defaultErrorHandler(error, ctx, response) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
623
|
+
// Determine status code
|
|
624
|
+
let status = error.code || 500;
|
|
625
|
+
let message = error.message || 'Internal Server Error';
|
|
626
|
+
let exposeDetails = false;
|
|
627
|
+
|
|
628
|
+
// Classify errors
|
|
629
|
+
if (error.name === 'ProtocolError' || error.name === 'KeypointError' ||
|
|
630
|
+
error.name === 'PolicyError' || error.name === 'ValidationError') {
|
|
631
|
+
exposeDetails = this.options.strictMode ? false : true;
|
|
632
|
+
} else if (status < 500) {
|
|
633
|
+
exposeDetails = true; // 4xx errors
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Prepare response
|
|
637
|
+
const errorResponse = {
|
|
638
|
+
status,
|
|
639
|
+
headers: {
|
|
640
|
+
'Content-Type': 'application/json',
|
|
641
|
+
...this.options.defaultResponseHeaders
|
|
642
|
+
},
|
|
643
|
+
body: {
|
|
644
|
+
error: exposeDetails ? message : (status >= 500 ? 'Internal Server Error' : message),
|
|
645
|
+
code: status,
|
|
646
|
+
timestamp: new Date().toISOString(),
|
|
647
|
+
requestId: ctx?.id
|
|
571
648
|
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// Add details if allowed
|
|
652
|
+
if (exposeDetails) {
|
|
653
|
+
if (error.details) errorResponse.body.details = error.details;
|
|
654
|
+
if (error.decision) errorResponse.body.decision = error.decision;
|
|
655
|
+
if (error.errors) errorResponse.body.errors = error.errors;
|
|
572
656
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
};
|
|
657
|
+
// Stack trace in development
|
|
658
|
+
if (process.env.NODE_ENV === 'development' && error.stack) {
|
|
659
|
+
errorResponse.body.stack = error.stack.split('\n').slice(0, 5).join('\n');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Log error
|
|
664
|
+
if (status >= 500) {
|
|
665
|
+
console.error(`Server Error [${status}]:`, {
|
|
666
|
+
message: error.message,
|
|
667
|
+
stack: error.stack,
|
|
668
|
+
requestId: ctx?.id,
|
|
669
|
+
path: ctx?.path
|
|
670
|
+
});
|
|
588
671
|
}
|
|
589
672
|
|
|
673
|
+
return errorResponse;
|
|
674
|
+
}
|
|
675
|
+
|
|
590
676
|
// Event system
|
|
591
677
|
|
|
592
678
|
on(event, handler) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Copyright AnasBex - 2026 TransformersPlugin.js
|
|
2
|
+
/*
|
|
3
|
+
|
|
4
|
+
API response standardization
|
|
5
|
+
|
|
6
|
+
Inject metadata (requestId, timestamp, duration)
|
|
7
|
+
|
|
8
|
+
Optional envelope (success, data, error)
|
|
9
|
+
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class TransformerPlugin {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = {
|
|
15
|
+
envelope: true,
|
|
16
|
+
addRequestId: true,
|
|
17
|
+
addTimestamp: true,
|
|
18
|
+
addDuration: true,
|
|
19
|
+
...options
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async process(ctx, next) {
|
|
24
|
+
const start = Date.now();
|
|
25
|
+
|
|
26
|
+
const response = await next(ctx);
|
|
27
|
+
|
|
28
|
+
// If handler returns null / undefined
|
|
29
|
+
if (!response) return response;
|
|
30
|
+
|
|
31
|
+
// If the response is not a KeypointJS API object
|
|
32
|
+
if (typeof response !== 'object' || !response.body) {
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const duration = Date.now() - start;
|
|
37
|
+
|
|
38
|
+
// Error response
|
|
39
|
+
if (response.status >= 400) {
|
|
40
|
+
return {
|
|
41
|
+
...response,
|
|
42
|
+
body: this.options.envelope
|
|
43
|
+
? {
|
|
44
|
+
success: false,
|
|
45
|
+
error: response.body,
|
|
46
|
+
meta: this._meta(ctx, duration)
|
|
47
|
+
}
|
|
48
|
+
: response.body
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Success response
|
|
53
|
+
return {
|
|
54
|
+
...response,
|
|
55
|
+
body: this.options.envelope
|
|
56
|
+
? {
|
|
57
|
+
success: true,
|
|
58
|
+
data: response.body,
|
|
59
|
+
meta: this._meta(ctx, duration)
|
|
60
|
+
}
|
|
61
|
+
: response.body
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_meta(ctx, duration) {
|
|
66
|
+
const meta = {};
|
|
67
|
+
|
|
68
|
+
if (this.options.addRequestId) {
|
|
69
|
+
meta.requestId = ctx.id;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.options.addTimestamp) {
|
|
73
|
+
meta.timestamp = new Date().toISOString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.options.addDuration) {
|
|
77
|
+
meta.durationMs = duration;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return meta;
|
|
81
|
+
}
|
|
82
|
+
}
|