keypointjs 1.0.0
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 +201 -0
- package/README.md +808 -0
- package/package.json +72 -0
- package/src/core/Context.js +104 -0
- package/src/core/ProtocolEngine.js +144 -0
- package/src/keypoint/Keypoint.js +36 -0
- package/src/keypoint/KeypointContext.js +88 -0
- package/src/keypoint/KeypointStorage.js +236 -0
- package/src/keypoint/KeypointValidator.js +51 -0
- package/src/keypoint/ScopeManager.js +206 -0
- package/src/keypointJS.js +779 -0
- package/src/plugins/AuditLogger.js +294 -0
- package/src/plugins/PluginManager.js +303 -0
- package/src/plugins/RateLimiter.js +24 -0
- package/src/plugins/WebSocketGuard.js +351 -0
- package/src/policy/AccessDecision.js +104 -0
- package/src/policy/PolicyEngine.js +82 -0
- package/src/policy/PolicyRule.js +246 -0
- package/src/router/MinimalRouter.js +41 -0
|
@@ -0,0 +1,779 @@
|
|
|
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
|
+
}
|
|
8
|
+
|
|
9
|
+
import { Context } from './core/Context.js';
|
|
10
|
+
import { ProtocolEngine, ProtocolError } from './core/ProtocolEngine.js';
|
|
11
|
+
import { Keypoint } from './keypoint/Keypoint.js';
|
|
12
|
+
import { KeypointContext } from './keypoint/KeypointContext.js';
|
|
13
|
+
import { KeypointValidator } from './keypoint/KeypointValidator.js';
|
|
14
|
+
import { MemoryKeypointStorage } from './keypoint/KeypointStorage.js';
|
|
15
|
+
import { ScopeManager } from './keypoint/ScopeManager.js';
|
|
16
|
+
import { PolicyEngine } from './policy/PolicyEngine.js';
|
|
17
|
+
import { BuiltInRules } from './policy/PolicyRule.js';
|
|
18
|
+
import { MinimalRouter } from './router/MinimalRouter.js';
|
|
19
|
+
import { PluginManager, BuiltInHooks } from './plugins/PluginManager.js';
|
|
20
|
+
import { RateLimiter } from './plugins/RateLimiter.js';
|
|
21
|
+
import { AuditLogger } from './plugins/AuditLogger.js';
|
|
22
|
+
import { WebSocketGuard } from './plugins/WebSocketGuard.js';
|
|
23
|
+
|
|
24
|
+
export class KeypointJS {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = {
|
|
27
|
+
requireKeypoint: true,
|
|
28
|
+
strictMode: true,
|
|
29
|
+
validateOrigin: true,
|
|
30
|
+
validateProtocol: true,
|
|
31
|
+
enableCORS: false,
|
|
32
|
+
corsOrigins: ['*'],
|
|
33
|
+
maxRequestSize: '10mb',
|
|
34
|
+
defaultResponseHeaders: {
|
|
35
|
+
'X-Powered-By': 'KeypointJS',
|
|
36
|
+
'X-Content-Type-Options': 'nosniff'
|
|
37
|
+
},
|
|
38
|
+
errorHandler: this.defaultErrorHandler.bind(this),
|
|
39
|
+
...options
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Initialize core components
|
|
43
|
+
this.initializeCore();
|
|
44
|
+
|
|
45
|
+
// Initialize layers
|
|
46
|
+
this.initializeLayers();
|
|
47
|
+
|
|
48
|
+
// Setup built-in policies
|
|
49
|
+
this.setupBuiltInPolicies();
|
|
50
|
+
|
|
51
|
+
// Event emitter
|
|
52
|
+
this.events = new Map();
|
|
53
|
+
|
|
54
|
+
// Statistics
|
|
55
|
+
this.stats = {
|
|
56
|
+
requests: 0,
|
|
57
|
+
successful: 0,
|
|
58
|
+
failed: 0,
|
|
59
|
+
keypointValidations: 0,
|
|
60
|
+
policyChecks: 0,
|
|
61
|
+
startTime: new Date()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
initializeCore() {
|
|
66
|
+
// Core protocol engine
|
|
67
|
+
this.protocolEngine = new ProtocolEngine({
|
|
68
|
+
maxBodySize: this.options.maxRequestSize,
|
|
69
|
+
parseJSON: true,
|
|
70
|
+
parseForm: true
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Keypoint system
|
|
74
|
+
this.keypointStorage = this.options.keypointStorage || new MemoryKeypointStorage();
|
|
75
|
+
this.scopeManager = new ScopeManager();
|
|
76
|
+
this.keypointValidator = new KeypointValidator(this.keypointStorage);
|
|
77
|
+
|
|
78
|
+
// Policy engine
|
|
79
|
+
this.policyEngine = new PolicyEngine();
|
|
80
|
+
|
|
81
|
+
// Router
|
|
82
|
+
this.router = new MinimalRouter();
|
|
83
|
+
|
|
84
|
+
// Plugin manager
|
|
85
|
+
this.pluginManager = new PluginManager();
|
|
86
|
+
|
|
87
|
+
// Middleware chain
|
|
88
|
+
this.middlewareChain = [];
|
|
89
|
+
|
|
90
|
+
// WebSocket support
|
|
91
|
+
this.wsGuard = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
initializeLayers() {
|
|
95
|
+
// Layer 0: Pre-processing (hooks)
|
|
96
|
+
this.use(async (ctx, next) => {
|
|
97
|
+
await this.pluginManager.runHook(BuiltInHooks.BEFORE_KEYPOINT_VALIDATION, ctx);
|
|
98
|
+
return next(ctx);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Layer 1: Protocol Engine
|
|
102
|
+
this.use(async (ctx, next) => {
|
|
103
|
+
try {
|
|
104
|
+
const processed = await this.protocolEngine.process(ctx.request);
|
|
105
|
+
|
|
106
|
+
ctx.id = processed.id;
|
|
107
|
+
ctx.timestamp = processed.timestamp;
|
|
108
|
+
ctx.metadata = processed.metadata;
|
|
109
|
+
|
|
110
|
+
// Update request object
|
|
111
|
+
if (processed.request) {
|
|
112
|
+
ctx.request = {
|
|
113
|
+
...ctx.request,
|
|
114
|
+
...processed.request
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ctx.setState('_protocol', processed.protocol);
|
|
119
|
+
ctx.setState('_ip', processed.request?.ip);
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new KeypointError(`Protocol error: ${error.message}`, 400);
|
|
123
|
+
}
|
|
124
|
+
return next(ctx);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Layer 2: CORS (if enabled)
|
|
128
|
+
if (this.options.enableCORS) {
|
|
129
|
+
this.use(this.corsMiddleware.bind(this));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Layer 3: Keypoint Validation (if required)
|
|
133
|
+
if (this.options.requireKeypoint) {
|
|
134
|
+
this.use(async (ctx, next) => {
|
|
135
|
+
await this.pluginManager.runHook(BuiltInHooks.BEFORE_KEYPOINT_VALIDATION, ctx);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const isValid = await this.keypointValidator.validate(ctx);
|
|
139
|
+
if (!isValid) {
|
|
140
|
+
throw new KeypointError('Invalid or missing keypoint', 401);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.stats.keypointValidations++;
|
|
144
|
+
|
|
145
|
+
// Validate origin if configured
|
|
146
|
+
if (this.options.validateOrigin && !ctx.validateOrigin()) {
|
|
147
|
+
throw new KeypointError('Origin not allowed for this keypoint', 403);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate protocol if configured
|
|
151
|
+
if (this.options.validateProtocol && !ctx.validateProtocol()) {
|
|
152
|
+
throw new KeypointError('Protocol not allowed for this keypoint', 403);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await this.pluginManager.runHook(BuiltInHooks.AFTER_KEYPOINT_VALIDATION, ctx);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await this.pluginManager.runHook(BuiltInHooks.ON_ERROR, ctx, error);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return next(ctx);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Layer 4: Policy Check
|
|
166
|
+
this.use(async (ctx, next) => {
|
|
167
|
+
await this.pluginManager.runHook(BuiltInHooks.BEFORE_POLICY_CHECK, ctx);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const decision = await this.policyEngine.evaluate(ctx);
|
|
171
|
+
if (!decision.allowed) {
|
|
172
|
+
throw new PolicyError(decision.reason, 403, decision);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
ctx.policyDecision = decision;
|
|
176
|
+
this.stats.policyChecks++;
|
|
177
|
+
|
|
178
|
+
await this.pluginManager.runHook(BuiltInHooks.AFTER_POLICY_CHECK, ctx, decision);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
await this.pluginManager.runHook(BuiltInHooks.ON_ERROR, ctx, error);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return next(ctx);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Layer 5: Plugin Processing
|
|
188
|
+
this.use(async (ctx, next) => {
|
|
189
|
+
return this.pluginManager.process(ctx, next);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Layer 6: Route Execution
|
|
193
|
+
this.use(async (ctx, next) => {
|
|
194
|
+
await this.pluginManager.runHook(BuiltInHooks.BEFORE_ROUTE_EXECUTION, ctx);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await this.router.handle(ctx);
|
|
198
|
+
|
|
199
|
+
await this.pluginManager.runHook(BuiltInHooks.AFTER_ROUTE_EXECUTION, ctx);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await this.pluginManager.runHook(BuiltInHooks.ON_ERROR, ctx, error);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return next(ctx);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Layer 7: Response Processing
|
|
209
|
+
this.use(async (ctx, next) => {
|
|
210
|
+
await this.pluginManager.runHook(BuiltInHooks.BEFORE_RESPONSE, ctx);
|
|
211
|
+
|
|
212
|
+
// Apply default headers
|
|
213
|
+
if (ctx.response) {
|
|
214
|
+
ctx.response.headers = {
|
|
215
|
+
...this.options.defaultResponseHeaders,
|
|
216
|
+
...ctx.response.headers
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Add security headers
|
|
220
|
+
ctx.response.headers['X-Keypoint-ID'] = ctx.getKeypointId() || 'none';
|
|
221
|
+
ctx.response.headers['X-Policy-Decision'] = ctx.policyDecision?.allowed ? 'allowed' : 'denied';
|
|
222
|
+
|
|
223
|
+
// Add CORS headers if enabled
|
|
224
|
+
if (this.options.enableCORS) {
|
|
225
|
+
this.addCORSHeaders(ctx);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await this.pluginManager.runHook(BuiltInHooks.AFTER_RESPONSE, ctx);
|
|
230
|
+
|
|
231
|
+
return next(ctx);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setupBuiltInPolicies() {
|
|
236
|
+
// Add rate limiting policy
|
|
237
|
+
const rateLimitRule = BuiltInRules.rateLimitRule(100, 60);
|
|
238
|
+
this.policyEngine.addRule(rateLimitRule);
|
|
239
|
+
|
|
240
|
+
// Add method validation policy
|
|
241
|
+
const methodRule = BuiltInRules.methodRule(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']);
|
|
242
|
+
this.policyEngine.addRule(methodRule);
|
|
243
|
+
|
|
244
|
+
// Add built-in policy templates
|
|
245
|
+
this.policyEngine.addPolicy('public', this.policyEngine.allow({
|
|
246
|
+
scope: 'api:public',
|
|
247
|
+
protocol: 'https'
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
this.policyEngine.addPolicy('admin', this.policyEngine.allow({
|
|
251
|
+
scope: 'admin',
|
|
252
|
+
protocol: 'https'
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
this.policyEngine.addPolicy('internal', this.policyEngine.allow({
|
|
256
|
+
scope: 'api:internal',
|
|
257
|
+
protocol: ['https', 'wss']
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
corsMiddleware(ctx, next) {
|
|
262
|
+
const origin = ctx.getHeader('origin');
|
|
263
|
+
|
|
264
|
+
if (origin) {
|
|
265
|
+
// Check if origin is allowed
|
|
266
|
+
const isAllowed = this.options.corsOrigins.includes('*') ||
|
|
267
|
+
this.options.corsOrigins.includes(origin);
|
|
268
|
+
|
|
269
|
+
if (isAllowed) {
|
|
270
|
+
ctx.response.headers = {
|
|
271
|
+
...ctx.response.headers,
|
|
272
|
+
'Access-Control-Allow-Origin': origin,
|
|
273
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
274
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
275
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Keypoint-ID, X-Keypoint-Secret'
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Handle preflight requests
|
|
281
|
+
if (ctx.method === 'OPTIONS') {
|
|
282
|
+
ctx.response = {
|
|
283
|
+
status: 204,
|
|
284
|
+
headers: ctx.response.headers
|
|
285
|
+
};
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return next(ctx);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
addCORSHeaders(ctx) {
|
|
293
|
+
if (!this.options.enableCORS) return;
|
|
294
|
+
|
|
295
|
+
const origin = ctx.getHeader('origin');
|
|
296
|
+
if (origin && (this.options.corsOrigins.includes('*') || this.options.corsOrigins.includes(origin))) {
|
|
297
|
+
ctx.response.headers = {
|
|
298
|
+
...ctx.response.headers,
|
|
299
|
+
'Access-Control-Allow-Origin': origin,
|
|
300
|
+
'Access-Control-Expose-Headers': 'X-Keypoint-ID, X-Policy-Decision'
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Public API Methods
|
|
306
|
+
|
|
307
|
+
use(middleware) {
|
|
308
|
+
this.middlewareChain.push(middleware);
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
route(method, path, handler) {
|
|
313
|
+
this.router.route(method, path, handler);
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
get(path, handler) {
|
|
318
|
+
return this.route('GET', path, handler);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
post(path, handler) {
|
|
322
|
+
return this.route('POST', path, handler);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
put(path, handler) {
|
|
326
|
+
return this.route('PUT', path, handler);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
delete(path, handler) {
|
|
330
|
+
return this.route('DELETE', path, handler);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
patch(path, handler) {
|
|
334
|
+
return this.route('PATCH', path, handler);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
options(path, handler) {
|
|
338
|
+
return this.route('OPTIONS', path, handler);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Plugin management
|
|
342
|
+
|
|
343
|
+
registerPlugin(plugin, options = {}) {
|
|
344
|
+
this.pluginManager.register(plugin, options);
|
|
345
|
+
|
|
346
|
+
// Special handling for WebSocketGuard
|
|
347
|
+
if (plugin instanceof WebSocketGuard) {
|
|
348
|
+
this.wsGuard = plugin;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
enableWebSocket(options = {}) {
|
|
355
|
+
if (!this.wsGuard) {
|
|
356
|
+
const wsGuard = new WebSocketGuard(options);
|
|
357
|
+
this.registerPlugin(wsGuard);
|
|
358
|
+
}
|
|
359
|
+
return this.wsGuard;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Keypoint management
|
|
363
|
+
|
|
364
|
+
async createKeypoint(data) {
|
|
365
|
+
const Keypoint = (await import('./keypoint/Keypoint.js')).Keypoint;
|
|
366
|
+
const keypoint = new Keypoint(data);
|
|
367
|
+
await this.keypointStorage.set(keypoint);
|
|
368
|
+
|
|
369
|
+
this.emit('keypoint:created', { keypoint });
|
|
370
|
+
return keypoint;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async revokeKeypoint(keyId) {
|
|
374
|
+
const keypoint = await this.keypointStorage.get(keyId);
|
|
375
|
+
if (keypoint) {
|
|
376
|
+
await this.keypointStorage.delete(keyId);
|
|
377
|
+
this.emit('keypoint:revoked', { keyId, keypoint });
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async listKeypoints(filter = {}) {
|
|
384
|
+
return await this.keypointStorage.list(filter);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async getKeypoint(keyId) {
|
|
388
|
+
return await this.keypointStorage.get(keyId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Policy management
|
|
392
|
+
|
|
393
|
+
addPolicyRule(rule) {
|
|
394
|
+
this.policyEngine.addRule(rule);
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
addPolicy(name, policyFn) {
|
|
399
|
+
this.policyEngine.addPolicy(name, policyFn);
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Scope management
|
|
404
|
+
|
|
405
|
+
defineScope(name, description, metadata = {}) {
|
|
406
|
+
this.scopeManager.defineScope(name, description, metadata);
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Request handling
|
|
411
|
+
|
|
412
|
+
async handleRequest(request, response) {
|
|
413
|
+
const ctx = new KeypointContext(request);
|
|
414
|
+
this.stats.requests++;
|
|
415
|
+
|
|
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
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async runMiddlewareChain(ctx, index = 0) {
|
|
441
|
+
if (index >= this.middlewareChain.length) return;
|
|
442
|
+
|
|
443
|
+
const middleware = this.middlewareChain[index];
|
|
444
|
+
const next = () => this.runMiddlewareChain(ctx, index + 1);
|
|
445
|
+
|
|
446
|
+
return await middleware(ctx, next);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// HTTP Server integration
|
|
450
|
+
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();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (this.wsGuard) {
|
|
474
|
+
this.wsGuard.attachToServer(server, this);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return server;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
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);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return server;
|
|
500
|
+
}
|
|
501
|
+
|
|
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
|
+
|
|
546
|
+
// Error handling
|
|
547
|
+
|
|
548
|
+
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
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
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
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Event system
|
|
591
|
+
|
|
592
|
+
on(event, handler) {
|
|
593
|
+
if (!this.events.has(event)) {
|
|
594
|
+
this.events.set(event, []);
|
|
595
|
+
}
|
|
596
|
+
this.events.get(event).push(handler);
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
off(event, handler) {
|
|
601
|
+
if (!this.events.has(event)) return this;
|
|
602
|
+
|
|
603
|
+
const handlers = this.events.get(event);
|
|
604
|
+
const index = handlers.indexOf(handler);
|
|
605
|
+
if (index !== -1) {
|
|
606
|
+
handlers.splice(index, 1);
|
|
607
|
+
}
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
emit(event, data) {
|
|
612
|
+
if (!this.events.has(event)) return;
|
|
613
|
+
|
|
614
|
+
for (const handler of this.events.get(event)) {
|
|
615
|
+
try {
|
|
616
|
+
handler(data);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Statistics
|
|
624
|
+
|
|
625
|
+
getStats() {
|
|
626
|
+
const uptime = Date.now() - this.stats.startTime;
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
...this.stats,
|
|
630
|
+
uptime,
|
|
631
|
+
uptimeFormatted: this.formatUptime(uptime),
|
|
632
|
+
successRate: this.stats.requests > 0
|
|
633
|
+
? (this.stats.successful / this.stats.requests * 100).toFixed(2) + '%'
|
|
634
|
+
: '0%',
|
|
635
|
+
plugins: this.pluginManager.getStats(),
|
|
636
|
+
keypoints: {
|
|
637
|
+
total: this.keypointStorage.store.size,
|
|
638
|
+
expired: (async () => {
|
|
639
|
+
const all = await this.keypointStorage.list();
|
|
640
|
+
return all.filter(k => k.isExpired()).length;
|
|
641
|
+
})(),
|
|
642
|
+
active: (async () => {
|
|
643
|
+
const all = await this.keypointStorage.list();
|
|
644
|
+
return all.filter(k => !k.isExpired()).length;
|
|
645
|
+
})()
|
|
646
|
+
},
|
|
647
|
+
routes: this.router.routes.size,
|
|
648
|
+
policies: this.policyEngine.rules.length
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
formatUptime(ms) {
|
|
653
|
+
const seconds = Math.floor(ms / 1000);
|
|
654
|
+
const days = Math.floor(seconds / 86400);
|
|
655
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
656
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
657
|
+
const secs = seconds % 60;
|
|
658
|
+
|
|
659
|
+
const parts = [];
|
|
660
|
+
if (days > 0) parts.push(`${days}d`);
|
|
661
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
662
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
663
|
+
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
|
|
664
|
+
|
|
665
|
+
return parts.join(' ');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Health check
|
|
669
|
+
|
|
670
|
+
async healthCheck() {
|
|
671
|
+
const checks = {
|
|
672
|
+
keypointStorage: this.keypointStorage instanceof MemoryKeypointStorage ? 'memory' : 'connected',
|
|
673
|
+
pluginManager: 'ok',
|
|
674
|
+
policyEngine: 'ok',
|
|
675
|
+
router: 'ok',
|
|
676
|
+
uptime: this.getStats().uptimeFormatted
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// Check storage connectivity if not memory
|
|
680
|
+
if (!(this.keypointStorage instanceof MemoryKeypointStorage)) {
|
|
681
|
+
try {
|
|
682
|
+
await this.keypointStorage.count();
|
|
683
|
+
checks.keypointStorage = 'connected';
|
|
684
|
+
} catch (error) {
|
|
685
|
+
checks.keypointStorage = 'disconnected';
|
|
686
|
+
checks.error = error.message;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const allOk = Object.values(checks).every(v => v !== 'disconnected');
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
status: allOk ? 'healthy' : 'degraded',
|
|
694
|
+
timestamp: new Date().toISOString(),
|
|
695
|
+
checks
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Configuration
|
|
700
|
+
|
|
701
|
+
configure(options) {
|
|
702
|
+
this.options = { ...this.options, ...options };
|
|
703
|
+
return this;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Shutdown
|
|
707
|
+
|
|
708
|
+
async shutdown() {
|
|
709
|
+
console.log('Shutting down KeypointJS...');
|
|
710
|
+
|
|
711
|
+
// Shutdown plugins
|
|
712
|
+
await this.pluginManager.shutdown();
|
|
713
|
+
|
|
714
|
+
// Close WebSocket connections
|
|
715
|
+
if (this.wsGuard) {
|
|
716
|
+
this.wsGuard.cleanup();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Emit shutdown event
|
|
720
|
+
this.emit('shutdown', {
|
|
721
|
+
timestamp: new Date(),
|
|
722
|
+
stats: this.getStats()
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
console.log('KeypointJS shutdown complete');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Custom Error Classes
|
|
730
|
+
|
|
731
|
+
export class KeypointError extends Error {
|
|
732
|
+
constructor(message, code = 401, details = {}) {
|
|
733
|
+
super(message);
|
|
734
|
+
this.name = 'KeypointError';
|
|
735
|
+
this.code = code;
|
|
736
|
+
this.details = details;
|
|
737
|
+
this.timestamp = new Date();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export class PolicyError extends Error {
|
|
742
|
+
constructor(message, code = 403, decision = null) {
|
|
743
|
+
super(message);
|
|
744
|
+
this.name = 'PolicyError';
|
|
745
|
+
this.code = code;
|
|
746
|
+
this.decision = decision;
|
|
747
|
+
this.timestamp = new Date();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export class ValidationError extends Error {
|
|
752
|
+
constructor(message, code = 400, errors = []) {
|
|
753
|
+
super(message);
|
|
754
|
+
this.name = 'ValidationError';
|
|
755
|
+
this.code = code;
|
|
756
|
+
this.errors = errors;
|
|
757
|
+
this.timestamp = new Date();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Export utilities
|
|
762
|
+
|
|
763
|
+
export {
|
|
764
|
+
Context,
|
|
765
|
+
ProtocolEngine,
|
|
766
|
+
Keypoint,
|
|
767
|
+
KeypointContext,
|
|
768
|
+
KeypointValidator,
|
|
769
|
+
MemoryKeypointStorage,
|
|
770
|
+
ScopeManager,
|
|
771
|
+
PolicyEngine,
|
|
772
|
+
BuiltInRules,
|
|
773
|
+
MinimalRouter,
|
|
774
|
+
PluginManager,
|
|
775
|
+
BuiltInHooks,
|
|
776
|
+
RateLimiter,
|
|
777
|
+
AuditLogger,
|
|
778
|
+
WebSocketGuard
|
|
779
|
+
};
|