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.
@@ -0,0 +1,294 @@
1
+ export class AuditLogger {
2
+ constructor(options = {}) {
3
+ this.options = {
4
+ logLevel: 'info',
5
+ logToConsole: true,
6
+ logToFile: false,
7
+ filePath: './audit.log',
8
+ maxFileSize: '10mb',
9
+ ...options
10
+ };
11
+
12
+ this.levels = {
13
+ debug: 0,
14
+ info: 1,
15
+ warn: 2,
16
+ error: 3,
17
+ critical: 4
18
+ };
19
+
20
+ this.logs = [];
21
+ this.rotationInterval = null;
22
+
23
+ if (this.options.logToFile) {
24
+ this.setupFileLogging();
25
+ }
26
+ }
27
+
28
+ async process(context, next) {
29
+ const startTime = Date.now();
30
+
31
+ try {
32
+ // Add audit data to context
33
+ context.audit = {
34
+ requestId: context.id,
35
+ keypointId: context.getKeypointId(),
36
+ timestamp: new Date(),
37
+ action: `${context.method} ${context.path}`,
38
+ status: 'processing'
39
+ };
40
+
41
+ const result = await next(context);
42
+ const duration = Date.now() - startTime;
43
+
44
+ // Log successful request
45
+ await this.logRequest(context, {
46
+ status: 'success',
47
+ duration,
48
+ responseStatus: context.response?.status
49
+ });
50
+
51
+ return result;
52
+ } catch (error) {
53
+ const duration = Date.now() - startTime;
54
+
55
+ // Log failed request
56
+ await this.logRequest(context, {
57
+ status: 'error',
58
+ duration,
59
+ error: error.message,
60
+ errorCode: error.code,
61
+ stack: this.options.logLevel === 'debug' ? error.stack : undefined
62
+ });
63
+
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ async logRequest(context, details = {}) {
69
+ const logEntry = {
70
+ timestamp: new Date().toISOString(),
71
+ level: details.status === 'error' ? 'error' : 'info',
72
+ requestId: context.id,
73
+ keypointId: context.getKeypointId(),
74
+ ip: context.ip,
75
+ userAgent: context.getHeader('user-agent'),
76
+ method: context.method,
77
+ path: context.path,
78
+ protocol: context.protocol,
79
+ scopes: context.keypoint?.scopes || [],
80
+ ...details,
81
+ metadata: {
82
+ ...context.getKeypointMetadata(),
83
+ ...details.metadata
84
+ }
85
+ };
86
+
87
+ // Add to memory buffer
88
+ this.logs.push(logEntry);
89
+
90
+ // Log to console
91
+ if (this.options.logToConsole) {
92
+ this.logToConsole(logEntry);
93
+ }
94
+
95
+ // Log to file
96
+ if (this.options.logToFile) {
97
+ await this.logToFile(logEntry);
98
+ }
99
+
100
+ // Trigger event
101
+ this.emit('audit', logEntry);
102
+
103
+ return logEntry;
104
+ }
105
+
106
+ logToConsole(entry) {
107
+ const color = {
108
+ info: '\x1b[32m', // Green
109
+ warn: '\x1b[33m', // Yellow
110
+ error: '\x1b[31m', // Red
111
+ debug: '\x1b[36m' // Cyan
112
+ } [entry.level] || '\x1b[0m';
113
+
114
+ const reset = '\x1b[0m';
115
+
116
+ const message = [
117
+ `${color}[${entry.timestamp}]`,
118
+ `${entry.level.toUpperCase()}`,
119
+ `${entry.method} ${entry.path}`,
120
+ `${entry.status || ''}`,
121
+ `${entry.duration ? `${entry.duration}ms` : ''}`,
122
+ entry.error ? `- ${entry.error}` : '',
123
+ reset
124
+ ].filter(Boolean).join(' ');
125
+
126
+ console.log(message);
127
+ }
128
+
129
+ async logToFile(entry) {
130
+ const fs = await import('fs/promises');
131
+
132
+ try {
133
+ const line = JSON.stringify(entry) + '\n';
134
+ await fs.appendFile(this.options.filePath, line, 'utf-8');
135
+
136
+ // Check file size for rotation
137
+ await this.checkFileRotation();
138
+ } catch (error) {
139
+ console.error('Failed to write audit log:', error);
140
+ }
141
+ }
142
+
143
+ async checkFileRotation() {
144
+ const fs = await import('fs/promises');
145
+
146
+ try {
147
+ const stats = await fs.stat(this.options.filePath);
148
+ const maxSize = this.parseSize(this.options.maxFileSize);
149
+
150
+ if (stats.size > maxSize) {
151
+ await this.rotateLogFile();
152
+ }
153
+ } catch (error) {
154
+ // File doesn't exist or other error
155
+ }
156
+ }
157
+
158
+ async rotateLogFile() {
159
+ const fs = await import('fs/promises');
160
+ const path = await import('path');
161
+
162
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
163
+ const oldPath = this.options.filePath;
164
+ const newPath = path.join(
165
+ path.dirname(oldPath),
166
+ `${path.basename(oldPath, '.log')}-${timestamp}.log`
167
+ );
168
+
169
+ try {
170
+ await fs.rename(oldPath, newPath);
171
+ console.log(`Rotated audit log to ${newPath}`);
172
+ } catch (error) {
173
+ console.error('Failed to rotate log file:', error);
174
+ }
175
+ }
176
+
177
+ parseSize(size) {
178
+ const match = size.match(/^(\d+)(mb|kb|b)$/i);
179
+ if (!match) return 10 * 1024 * 1024; // 10MB default
180
+
181
+ const [, num, unit] = match;
182
+ const multiplier = {
183
+ 'b': 1,
184
+ 'kb': 1024,
185
+ 'mb': 1024 * 1024
186
+ } [unit.toLowerCase()];
187
+
188
+ return parseInt(num) * multiplier;
189
+ }
190
+
191
+ setupFileLogging() {
192
+ // Setup periodic rotation check
193
+ this.rotationInterval = setInterval(() => {
194
+ this.checkFileRotation();
195
+ }, 60000); // Check every minute
196
+
197
+ // Ensure log directory exists
198
+ this.ensureLogDirectory();
199
+ }
200
+
201
+ async ensureLogDirectory() {
202
+ const fs = await import('fs/promises');
203
+ const path = await import('path');
204
+
205
+ const dir = path.dirname(this.options.filePath);
206
+
207
+ try {
208
+ await fs.access(dir);
209
+ } catch {
210
+ await fs.mkdir(dir, { recursive: true });
211
+ }
212
+ }
213
+
214
+ async queryLogs(filter = {}) {
215
+ let filtered = this.logs;
216
+
217
+ if (filter.startDate) {
218
+ const start = new Date(filter.startDate);
219
+ filtered = filtered.filter(log => new Date(log.timestamp) >= start);
220
+ }
221
+
222
+ if (filter.endDate) {
223
+ const end = new Date(filter.endDate);
224
+ filtered = filtered.filter(log => new Date(log.timestamp) <= end);
225
+ }
226
+
227
+ if (filter.level) {
228
+ filtered = filtered.filter(log => log.level === filter.level);
229
+ }
230
+
231
+ if (filter.keypointId) {
232
+ filtered = filtered.filter(log => log.keypointId === filter.keypointId);
233
+ }
234
+
235
+ if (filter.ip) {
236
+ filtered = filtered.filter(log => log.ip === filter.ip);
237
+ }
238
+
239
+ if (filter.method) {
240
+ filtered = filtered.filter(log => log.method === filter.method);
241
+ }
242
+
243
+ if (filter.path) {
244
+ filtered = filtered.filter(log => log.path.includes(filter.path));
245
+ }
246
+
247
+ // Sort and limit
248
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
249
+
250
+ if (filter.limit) {
251
+ filtered = filtered.slice(0, filter.limit);
252
+ }
253
+
254
+ return {
255
+ total: this.logs.length,
256
+ filtered: filtered.length,
257
+ logs: filtered
258
+ };
259
+ }
260
+
261
+ clearLogs() {
262
+ const count = this.logs.length;
263
+ this.logs = [];
264
+ return count;
265
+ }
266
+
267
+ // Event emitter methods
268
+ on(event, handler) {
269
+ if (!this.listeners) this.listeners = new Map();
270
+ if (!this.listeners.has(event)) {
271
+ this.listeners.set(event, []);
272
+ }
273
+ this.listeners.get(event).push(handler);
274
+ }
275
+
276
+ emit(event, data) {
277
+ if (!this.listeners || !this.listeners.has(event)) return;
278
+
279
+ for (const handler of this.listeners.get(event)) {
280
+ try {
281
+ handler(data);
282
+ } catch (error) {
283
+ console.error(`Error in audit event handler:`, error);
284
+ }
285
+ }
286
+ }
287
+
288
+ // Cleanup
289
+ cleanup() {
290
+ if (this.rotationInterval) {
291
+ clearInterval(this.rotationInterval);
292
+ }
293
+ }
294
+ }
@@ -0,0 +1,303 @@
1
+ export class PluginManager {
2
+ constructor() {
3
+ this.plugins = new Map();
4
+ this.middlewareChain = [];
5
+ this.events = new Map();
6
+ this.hooks = new Map();
7
+ }
8
+
9
+ register(plugin, options = {}) {
10
+ const pluginName = plugin.constructor.name;
11
+
12
+ if (this.plugins.has(pluginName)) {
13
+ throw new Error(`Plugin ${pluginName} already registered`);
14
+ }
15
+
16
+ // Initialize plugin
17
+ plugin.name = pluginName;
18
+ plugin.options = options;
19
+ plugin.enabled = true;
20
+
21
+ this.plugins.set(pluginName, plugin);
22
+
23
+ // Add plugin's middleware if it has process method
24
+ if (typeof plugin.process === 'function') {
25
+ this.middlewareChain.push(async (ctx, next) => {
26
+ if (!plugin.enabled) return next(ctx);
27
+ return plugin.process(ctx, next);
28
+ });
29
+ }
30
+
31
+ // Register plugin events
32
+ if (typeof plugin.on === 'function') {
33
+ plugin.on('*', (event, data) => {
34
+ this.emit(event, data);
35
+ });
36
+ }
37
+
38
+ // Setup hooks
39
+ if (plugin.hooks) {
40
+ for (const [hookName, hookFn] of Object.entries(plugin.hooks)) {
41
+ this.addHook(hookName, hookFn);
42
+ }
43
+ }
44
+
45
+ console.log(`Plugin registered: ${pluginName}`);
46
+ return this;
47
+ }
48
+
49
+ unregister(pluginName) {
50
+ const plugin = this.plugins.get(pluginName);
51
+ if (!plugin) return false;
52
+
53
+ // Remove middleware
54
+ const index = this.middlewareChain.findIndex(middleware => {
55
+ // Find middleware that uses this plugin
56
+ return middleware.toString().includes(pluginName);
57
+ });
58
+
59
+ if (index !== -1) {
60
+ this.middlewareChain.splice(index, 1);
61
+ }
62
+
63
+ // Cleanup plugin
64
+ if (typeof plugin.cleanup === 'function') {
65
+ plugin.cleanup();
66
+ }
67
+
68
+ this.plugins.delete(pluginName);
69
+ console.log(`Plugin unregistered: ${pluginName}`);
70
+ return true;
71
+ }
72
+
73
+ enable(pluginName) {
74
+ const plugin = this.plugins.get(pluginName);
75
+ if (plugin) {
76
+ plugin.enabled = true;
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ disable(pluginName) {
83
+ const plugin = this.plugins.get(pluginName);
84
+ if (plugin) {
85
+ plugin.enabled = false;
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ async process(context, next) {
92
+ // Run through plugin middleware chain
93
+ let index = 0;
94
+
95
+ const runNext = async () => {
96
+ if (index >= this.middlewareChain.length) {
97
+ return next(context);
98
+ }
99
+
100
+ const middleware = this.middlewareChain[index];
101
+ index++;
102
+
103
+ return middleware(context, runNext);
104
+ };
105
+
106
+ return runNext();
107
+ }
108
+
109
+ // Event system
110
+ on(event, handler) {
111
+ if (!this.events.has(event)) {
112
+ this.events.set(event, []);
113
+ }
114
+ this.events.get(event).push(handler);
115
+ return this;
116
+ }
117
+
118
+ off(event, handler) {
119
+ if (!this.events.has(event)) return;
120
+
121
+ const handlers = this.events.get(event);
122
+ const index = handlers.indexOf(handler);
123
+ if (index !== -1) {
124
+ handlers.splice(index, 1);
125
+ }
126
+ return this;
127
+ }
128
+
129
+ emit(event, data) {
130
+ if (!this.events.has(event)) return;
131
+
132
+ const handlers = this.events.get(event);
133
+ for (const handler of handlers) {
134
+ try {
135
+ handler(data);
136
+ } catch (error) {
137
+ console.error(`Error in event handler for ${event}:`, error);
138
+ }
139
+ }
140
+
141
+ // Also emit to wildcard handlers
142
+ if (this.events.has('*')) {
143
+ const wildcardHandlers = this.events.get('*');
144
+ for (const handler of wildcardHandlers) {
145
+ try {
146
+ handler(event, data);
147
+ } catch (error) {
148
+ console.error(`Error in wildcard event handler:`, error);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Hook system
155
+ addHook(hookName, hookFn) {
156
+ if (!this.hooks.has(hookName)) {
157
+ this.hooks.set(hookName, []);
158
+ }
159
+ this.hooks.get(hookName).push(hookFn);
160
+ return this;
161
+ }
162
+
163
+ async runHook(hookName, ...args) {
164
+ if (!this.hooks.has(hookName)) return [];
165
+
166
+ const results = [];
167
+ const hooks = this.hooks.get(hookName);
168
+
169
+ for (const hookFn of hooks) {
170
+ try {
171
+ const result = await hookFn(...args);
172
+ if (result !== undefined) {
173
+ results.push(result);
174
+ }
175
+ } catch (error) {
176
+ console.error(`Error in hook ${hookName}:`, error);
177
+ }
178
+ }
179
+
180
+ return results;
181
+ }
182
+
183
+ // Plugin management
184
+ getPlugin(pluginName) {
185
+ return this.plugins.get(pluginName);
186
+ }
187
+
188
+ getAllPlugins() {
189
+ return Array.from(this.plugins.values());
190
+ }
191
+
192
+ getPluginNames() {
193
+ return Array.from(this.plugins.keys());
194
+ }
195
+
196
+ getEnabledPlugins() {
197
+ return Array.from(this.plugins.values()).filter(p => p.enabled);
198
+ }
199
+
200
+ // Configuration
201
+ configurePlugin(pluginName, config) {
202
+ const plugin = this.plugins.get(pluginName);
203
+ if (!plugin) return false;
204
+
205
+ if (typeof plugin.configure === 'function') {
206
+ plugin.configure(config);
207
+ return true;
208
+ }
209
+
210
+ // Merge options if no configure method
211
+ plugin.options = { ...plugin.options, ...config };
212
+ return true;
213
+ }
214
+
215
+ // Statistics
216
+ getStats() {
217
+ return {
218
+ totalPlugins: this.plugins.size,
219
+ enabledPlugins: this.getEnabledPlugins().length,
220
+ disabledPlugins: this.plugins.size - this.getEnabledPlugins().length,
221
+ middlewareCount: this.middlewareChain.length,
222
+ eventCount: Array.from(this.events.values()).reduce((sum, handlers) => sum + handlers.length, 0),
223
+ hookCount: Array.from(this.hooks.values()).reduce((sum, hooks) => sum + hooks.length, 0),
224
+ plugins: this.getAllPlugins().map(p => ({
225
+ name: p.name,
226
+ enabled: p.enabled,
227
+ hasProcess: typeof p.process === 'function',
228
+ hasCleanup: typeof p.cleanup === 'function'
229
+ }))
230
+ };
231
+ }
232
+
233
+ // Lifecycle
234
+ async initialize() {
235
+ // Run initialization hooks
236
+ await this.runHook('initialize', this);
237
+
238
+ // Initialize all plugins
239
+ for (const plugin of this.plugins.values()) {
240
+ if (typeof plugin.initialize === 'function') {
241
+ try {
242
+ await plugin.initialize();
243
+ console.log(`Plugin initialized: ${plugin.name}`);
244
+ } catch (error) {
245
+ console.error(`Failed to initialize plugin ${plugin.name}:`, error);
246
+ }
247
+ }
248
+ }
249
+
250
+ this.emit('initialized', { timestamp: new Date() });
251
+ }
252
+
253
+ async shutdown() {
254
+ // Run shutdown hooks
255
+ await this.runHook('shutdown', this);
256
+
257
+ // Shutdown all plugins
258
+ for (const plugin of this.plugins.values()) {
259
+ if (typeof plugin.shutdown === 'function') {
260
+ try {
261
+ await plugin.shutdown();
262
+ console.log(`Plugin shutdown: ${plugin.name}`);
263
+ } catch (error) {
264
+ console.error(`Failed to shutdown plugin ${plugin.name}:`, error);
265
+ }
266
+ }
267
+ }
268
+
269
+ this.emit('shutdown', { timestamp: new Date() });
270
+ this.clear();
271
+ }
272
+
273
+ clear() {
274
+ this.plugins.clear();
275
+ this.middlewareChain = [];
276
+ this.events.clear();
277
+ this.hooks.clear();
278
+ }
279
+ }
280
+
281
+ // Built-in hooks
282
+ export const BuiltInHooks = {
283
+ // Request lifecycle hooks
284
+ BEFORE_KEYPOINT_VALIDATION: 'before_keypoint_validation',
285
+ AFTER_KEYPOINT_VALIDATION: 'after_keypoint_validation',
286
+ BEFORE_POLICY_CHECK: 'before_policy_check',
287
+ AFTER_POLICY_CHECK: 'after_policy_check',
288
+ BEFORE_ROUTE_EXECUTION: 'before_route_execution',
289
+ AFTER_ROUTE_EXECUTION: 'after_route_execution',
290
+ BEFORE_RESPONSE: 'before_response',
291
+ AFTER_RESPONSE: 'after_response',
292
+
293
+ // Error hooks
294
+ ON_ERROR: 'on_error',
295
+
296
+ // Plugin lifecycle hooks
297
+ PLUGIN_REGISTERED: 'plugin_registered',
298
+ PLUGIN_UNREGISTERED: 'plugin_unregistered',
299
+
300
+ // System hooks
301
+ INITIALIZE: 'initialize',
302
+ SHUTDOWN: 'shutdown'
303
+ };
@@ -0,0 +1,24 @@
1
+ export class RateLimiter {
2
+ constructor(options = {}) {
3
+ this.limits = new Map();
4
+ this.window = options.window || 60 * 1000; // 1 minute
5
+ }
6
+
7
+ async process(context, next) {
8
+ const { keypoint, request } = context;
9
+
10
+ if (!keypoint) return next(context);
11
+
12
+ const limitKey = `rate:${keypoint.keyId}:${Date.now() / this.window}`;
13
+
14
+ const current = this.limits.get(limitKey) || 0;
15
+ const limit = keypoint.rateLimit.requests;
16
+
17
+ if (current >= limit) {
18
+ throw new Error('Rate limit exceeded', 429);
19
+ }
20
+
21
+ this.limits.set(limitKey, current + 1);
22
+ return next(context);
23
+ }
24
+ }