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 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.0.0",
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
- const context = {
17
- id: crypto.randomUUID(),
18
- timestamp: new Date(),
19
- protocol: this.detectProtocol(request),
20
- request: {
21
- method: request.method,
22
- url: new URL(request.url, `http://${request.headers.host}`),
23
- headers: this.normalizeHeaders(request.headers),
24
- ip: this.extractIP(request),
25
- userAgent: request.headers['user-agent'] || '',
26
- body: null
27
- },
28
- metadata: {}
29
- };
30
-
31
- // Parse body based on content-type
32
- if (this.shouldParseBody(request)) {
33
- context.request.body = await this.parseRequestBody(request);
34
- }
35
-
36
- // Validate protocol
37
- if (!this.supportedProtocols.has(context.protocol)) {
38
- throw new ProtocolError(`Unsupported protocol: ${context.protocol}`);
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
- return context;
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
- const forwardedProto = request.headers['x-forwarded-proto'];
46
- if (forwardedProto) return forwardedProto;
47
-
48
- const isSecure = request.connection?.encrypted ||
49
- request.socket?.encrypted ||
50
- (request.headers['x-arr-ssl'] !== undefined);
51
-
52
- return isSecure ? 'https' : 'http';
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
- return new Promise((resolve, reject) => {
83
- let body = '';
84
- request.on('data', chunk => {
85
- body += chunk.toString();
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
- // Check size limit
88
- if (body.length > this.parseSizeLimit()) {
89
- request.destroy();
90
- reject(new ProtocolError('Request body too large'));
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
- request.on('end', () => {
95
- try {
96
- const contentType = request.headers['content-type'] || '';
97
-
98
- if (contentType.includes('application/json')) {
99
- resolve(JSON.parse(body));
100
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
101
- const params = new URLSearchParams(body);
102
- const result = {};
103
- for (const [key, value] of params) {
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
- } catch (error) {
111
- reject(new ProtocolError(`Failed to parse body: ${error.message}`));
186
+ resolve(result);
112
187
  }
113
- });
114
-
115
- request.on('error', reject);
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
- // Import http module once at top level
2
- let http;
3
- try {
4
- http = await import('http');
5
- } catch (error) {
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
- ctx.id = processed.id;
107
- ctx.timestamp = processed.timestamp;
108
- ctx.metadata = processed.metadata;
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
- if (processed.request) {
112
- ctx.request = {
113
- ...ctx.request,
114
- ...processed.request
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
- throw new KeypointError(`Protocol error: ${error.message}`, 400);
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
- const ctx = new KeypointContext(request);
414
- this.stats.requests++;
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
- try {
417
- await this.runMiddlewareChain(ctx);
418
- this.stats.successful++;
419
-
420
- this.emit('request:success', {
421
- ctx,
422
- timestamp: new Date(),
423
- duration: ctx.response?.duration || 0
424
- });
425
-
426
- return ctx.response;
427
- } catch (error) {
428
- this.stats.failed++;
429
-
430
- this.emit('request:error', {
431
- ctx,
432
- error,
433
- timestamp: new Date()
434
- });
435
-
436
- return this.options.errorHandler(error, ctx, response);
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
- if (!http) {
452
- throw new Error('HTTP module not available');
453
- }
454
-
455
- const server = http.createServer(async (req, res) => {
456
- const response = await this.handleRequest(req, res);
457
-
458
- res.statusCode = response.status || 200;
459
- Object.entries(response.headers || {}).forEach(([key, value]) => {
460
- res.setHeader(key, value);
461
- });
462
-
463
- if (response.body !== undefined) {
464
- const body = typeof response.body === 'string' ?
465
- response.body :
466
- JSON.stringify(response.body);
467
- res.end(body);
468
- } else {
469
- res.end();
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
- const server = this.createServer();
482
-
483
- server.listen(port, hostname, () => {
484
- const address = server.address();
485
- console.log(`
486
- KeypointJS Server Started
487
- ════════════════════════════════════════
488
- Address: ${hostname}:${port}
489
- Mode: ${this.options.requireKeypoint ? 'Strict (Keypoint Required)' : 'Permissive'}
490
- Protocols: HTTP/HTTPS${this.wsGuard ? ' + WebSocket' : ''}
491
- Plugins: ${this.pluginManager.getPluginNames().length} loaded
492
- Keypoints: ${this.keypointStorage.store.size} registered
493
- ════════════════════════════════════════
494
- `);
495
-
496
- if (callback) callback(server);
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
- const status = error.code || 500;
550
- const message = error.message || 'Internal Server Error';
551
-
552
- if (this.options.strictMode) {
553
- // Hide internal error details in production
554
- const safeMessage = status >= 500 && process.env.NODE_ENV === 'production'
555
- ? 'Internal Server Error'
556
- : message;
557
-
558
- return {
559
- status,
560
- headers: {
561
- 'Content-Type': 'application/json',
562
- ...this.options.defaultResponseHeaders
563
- },
564
- body: {
565
- error: safeMessage,
566
- code: status,
567
- timestamp: new Date().toISOString(),
568
- requestId: ctx?.id
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
- return {
574
- status,
575
- headers: {
576
- 'Content-Type': 'application/json',
577
- ...this.options.defaultResponseHeaders
578
- },
579
- body: {
580
- error: message,
581
- code: status,
582
- timestamp: new Date().toISOString(),
583
- requestId: ctx?.id,
584
- details: error.details || {},
585
- stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
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
+ }