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,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
|
+
}
|