@objectstack/core 0.9.1 → 0.9.2
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/{ENHANCED_FEATURES.md → ADVANCED_FEATURES.md} +13 -13
- package/CHANGELOG.md +7 -0
- package/PHASE2_IMPLEMENTATION.md +388 -0
- package/README.md +24 -11
- package/REFACTORING_SUMMARY.md +40 -0
- package/dist/api-registry-plugin.test.js +20 -20
- package/dist/dependency-resolver.d.ts +62 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.js +317 -0
- package/dist/dependency-resolver.test.d.ts +2 -0
- package/dist/dependency-resolver.test.d.ts.map +1 -0
- package/dist/dependency-resolver.test.js +241 -0
- package/dist/health-monitor.d.ts +65 -0
- package/dist/health-monitor.d.ts.map +1 -0
- package/dist/health-monitor.js +269 -0
- package/dist/health-monitor.test.d.ts +2 -0
- package/dist/health-monitor.test.d.ts.map +1 -0
- package/dist/health-monitor.test.js +68 -0
- package/dist/hot-reload.d.ts +79 -0
- package/dist/hot-reload.d.ts.map +1 -0
- package/dist/hot-reload.js +313 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/kernel-base.d.ts +2 -2
- package/dist/kernel-base.js +2 -2
- package/dist/kernel.d.ts +79 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +383 -73
- package/dist/kernel.test.js +373 -122
- package/dist/lite-kernel.d.ts +55 -0
- package/dist/lite-kernel.d.ts.map +1 -0
- package/dist/lite-kernel.js +112 -0
- package/dist/lite-kernel.test.d.ts +2 -0
- package/dist/lite-kernel.test.d.ts.map +1 -0
- package/dist/lite-kernel.test.js +161 -0
- package/dist/logger.d.ts +2 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +26 -7
- package/dist/plugin-loader.d.ts +11 -0
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +34 -10
- package/dist/plugin-loader.test.js +9 -0
- package/dist/security/index.d.ts +3 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +4 -0
- package/dist/security/permission-manager.d.ts +96 -0
- package/dist/security/permission-manager.d.ts.map +1 -0
- package/dist/security/permission-manager.js +235 -0
- package/dist/security/permission-manager.test.d.ts +2 -0
- package/dist/security/permission-manager.test.d.ts.map +1 -0
- package/dist/security/permission-manager.test.js +220 -0
- package/dist/security/sandbox-runtime.d.ts +115 -0
- package/dist/security/sandbox-runtime.d.ts.map +1 -0
- package/dist/security/sandbox-runtime.js +310 -0
- package/dist/security/security-scanner.d.ts +92 -0
- package/dist/security/security-scanner.d.ts.map +1 -0
- package/dist/security/security-scanner.js +273 -0
- package/examples/{enhanced-kernel-example.ts → kernel-features-example.ts} +6 -6
- package/examples/phase2-integration.ts +355 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +20 -20
- package/src/dependency-resolver.test.ts +287 -0
- package/src/dependency-resolver.ts +388 -0
- package/src/health-monitor.test.ts +81 -0
- package/src/health-monitor.ts +316 -0
- package/src/hot-reload.ts +388 -0
- package/src/index.ts +6 -1
- package/src/kernel-base.ts +2 -2
- package/src/kernel.test.ts +469 -134
- package/src/kernel.ts +464 -78
- package/src/lite-kernel.test.ts +200 -0
- package/src/lite-kernel.ts +135 -0
- package/src/logger.ts +28 -7
- package/src/plugin-loader.test.ts +10 -1
- package/src/plugin-loader.ts +42 -13
- package/src/security/index.ts +19 -0
- package/src/security/permission-manager.test.ts +256 -0
- package/src/security/permission-manager.ts +336 -0
- package/src/security/sandbox-runtime.ts +432 -0
- package/src/security/security-scanner.ts +365 -0
- package/dist/enhanced-kernel.d.ts +0 -103
- package/dist/enhanced-kernel.d.ts.map +0 -1
- package/dist/enhanced-kernel.js +0 -403
- package/dist/enhanced-kernel.test.d.ts +0 -2
- package/dist/enhanced-kernel.test.d.ts.map +0 -1
- package/dist/enhanced-kernel.test.js +0 -412
- package/src/enhanced-kernel.test.ts +0 -535
- package/src/enhanced-kernel.ts +0 -496
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { LiteKernel } from './lite-kernel';
|
|
3
|
+
import type { Plugin } from './types';
|
|
4
|
+
|
|
5
|
+
describe('LiteKernel with Configurable Logger', () => {
|
|
6
|
+
let kernel: LiteKernel;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
kernel = new LiteKernel();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('Logger Configuration', () => {
|
|
13
|
+
it('should create kernel with default logger', () => {
|
|
14
|
+
expect(kernel).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should create kernel with custom logger config', async () => {
|
|
18
|
+
const customKernel = new LiteKernel({
|
|
19
|
+
logger: {
|
|
20
|
+
level: 'debug',
|
|
21
|
+
format: 'pretty',
|
|
22
|
+
sourceLocation: true
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(customKernel).toBeDefined();
|
|
27
|
+
|
|
28
|
+
// Cleanup
|
|
29
|
+
await customKernel.bootstrap();
|
|
30
|
+
await customKernel.shutdown();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should create kernel with file logging config', async () => {
|
|
34
|
+
const fileKernel = new LiteKernel({
|
|
35
|
+
logger: {
|
|
36
|
+
level: 'info',
|
|
37
|
+
format: 'json',
|
|
38
|
+
file: '/tmp/test-kernel.log'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(fileKernel).toBeDefined();
|
|
43
|
+
|
|
44
|
+
// Cleanup
|
|
45
|
+
await fileKernel.bootstrap();
|
|
46
|
+
await fileKernel.shutdown();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Plugin Context Logger', () => {
|
|
51
|
+
it('should provide logger to plugins', async () => {
|
|
52
|
+
let loggerReceived = false;
|
|
53
|
+
|
|
54
|
+
const testPlugin: Plugin = {
|
|
55
|
+
name: 'test-plugin',
|
|
56
|
+
init: async (ctx) => {
|
|
57
|
+
if (ctx.logger) {
|
|
58
|
+
loggerReceived = true;
|
|
59
|
+
ctx.logger.info('Plugin initialized', { plugin: 'test-plugin' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
kernel.use(testPlugin);
|
|
65
|
+
await kernel.bootstrap();
|
|
66
|
+
|
|
67
|
+
expect(loggerReceived).toBe(true);
|
|
68
|
+
|
|
69
|
+
await kernel.shutdown();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should allow plugins to use all log levels', async () => {
|
|
73
|
+
const logCalls: string[] = [];
|
|
74
|
+
|
|
75
|
+
const loggingPlugin: Plugin = {
|
|
76
|
+
name: 'logging-plugin',
|
|
77
|
+
init: async (ctx) => {
|
|
78
|
+
ctx.logger.debug('Debug message');
|
|
79
|
+
logCalls.push('debug');
|
|
80
|
+
|
|
81
|
+
ctx.logger.info('Info message');
|
|
82
|
+
logCalls.push('info');
|
|
83
|
+
|
|
84
|
+
ctx.logger.warn('Warning message');
|
|
85
|
+
logCalls.push('warn');
|
|
86
|
+
|
|
87
|
+
ctx.logger.error('Error message');
|
|
88
|
+
logCalls.push('error');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
kernel.use(loggingPlugin);
|
|
93
|
+
await kernel.bootstrap();
|
|
94
|
+
|
|
95
|
+
expect(logCalls).toContain('debug');
|
|
96
|
+
expect(logCalls).toContain('info');
|
|
97
|
+
expect(logCalls).toContain('warn');
|
|
98
|
+
expect(logCalls).toContain('error');
|
|
99
|
+
|
|
100
|
+
await kernel.shutdown();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should support metadata in logs', async () => {
|
|
104
|
+
const metadataPlugin: Plugin = {
|
|
105
|
+
name: 'metadata-plugin',
|
|
106
|
+
init: async (ctx) => {
|
|
107
|
+
ctx.logger.info('User action', {
|
|
108
|
+
userId: '123',
|
|
109
|
+
action: 'create',
|
|
110
|
+
resource: 'document'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
kernel.use(metadataPlugin);
|
|
116
|
+
await kernel.bootstrap();
|
|
117
|
+
|
|
118
|
+
await kernel.shutdown();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Kernel Lifecycle Logging', () => {
|
|
123
|
+
it('should log bootstrap process', async () => {
|
|
124
|
+
const plugin: Plugin = {
|
|
125
|
+
name: 'lifecycle-test',
|
|
126
|
+
init: async () => {
|
|
127
|
+
// Init logic
|
|
128
|
+
},
|
|
129
|
+
start: async () => {
|
|
130
|
+
// Start logic
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
kernel.use(plugin);
|
|
135
|
+
await kernel.bootstrap();
|
|
136
|
+
|
|
137
|
+
expect(kernel.isRunning()).toBe(true);
|
|
138
|
+
|
|
139
|
+
await kernel.shutdown();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should log shutdown process', async () => {
|
|
143
|
+
const plugin: Plugin = {
|
|
144
|
+
name: 'shutdown-test',
|
|
145
|
+
init: async () => {},
|
|
146
|
+
destroy: async () => {
|
|
147
|
+
// Cleanup
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
kernel.use(plugin);
|
|
152
|
+
await kernel.bootstrap();
|
|
153
|
+
await kernel.shutdown();
|
|
154
|
+
|
|
155
|
+
expect(kernel.getState()).toBe('stopped');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Environment Compatibility', () => {
|
|
160
|
+
it('should work in Node.js environment', async () => {
|
|
161
|
+
const nodeKernel = new LiteKernel({
|
|
162
|
+
logger: {
|
|
163
|
+
level: 'info',
|
|
164
|
+
format: 'json'
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const plugin: Plugin = {
|
|
169
|
+
name: 'node-test',
|
|
170
|
+
init: async (ctx) => {
|
|
171
|
+
ctx.logger.info('Running in Node.js');
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
nodeKernel.use(plugin);
|
|
176
|
+
await nodeKernel.bootstrap();
|
|
177
|
+
await nodeKernel.shutdown();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should support browser-friendly logging', async () => {
|
|
181
|
+
const browserKernel = new LiteKernel({
|
|
182
|
+
logger: {
|
|
183
|
+
level: 'info',
|
|
184
|
+
format: 'pretty'
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const plugin: Plugin = {
|
|
189
|
+
name: 'browser-test',
|
|
190
|
+
init: async (ctx) => {
|
|
191
|
+
ctx.logger.info('Browser-friendly format');
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
browserKernel.use(plugin);
|
|
196
|
+
await browserKernel.bootstrap();
|
|
197
|
+
await browserKernel.shutdown();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Plugin } from './types.js';
|
|
2
|
+
import { createLogger, ObjectLogger } from './logger.js';
|
|
3
|
+
import type { LoggerConfig } from '@objectstack/spec/system';
|
|
4
|
+
import { ObjectKernelBase } from './kernel-base.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ObjectKernel - MiniKernel Architecture
|
|
8
|
+
*
|
|
9
|
+
* A highly modular, plugin-based microkernel that:
|
|
10
|
+
* - Manages plugin lifecycle (init, start, destroy)
|
|
11
|
+
* - Provides dependency injection via service registry
|
|
12
|
+
* - Implements event/hook system for inter-plugin communication
|
|
13
|
+
* - Handles dependency resolution (topological sort)
|
|
14
|
+
* - Provides configurable logging for server and browser
|
|
15
|
+
*
|
|
16
|
+
* Core philosophy:
|
|
17
|
+
* - Business logic is completely separated into plugins
|
|
18
|
+
* - Kernel only manages lifecycle, DI, and hooks
|
|
19
|
+
* - Plugins are loaded as equal building blocks
|
|
20
|
+
*/
|
|
21
|
+
export class LiteKernel extends ObjectKernelBase {
|
|
22
|
+
constructor(config?: { logger?: Partial<LoggerConfig> }) {
|
|
23
|
+
const logger = createLogger(config?.logger);
|
|
24
|
+
super(logger);
|
|
25
|
+
|
|
26
|
+
// Initialize context after logger is created
|
|
27
|
+
this.context = this.createContext();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a plugin
|
|
32
|
+
* @param plugin - Plugin instance
|
|
33
|
+
*/
|
|
34
|
+
use(plugin: Plugin): this {
|
|
35
|
+
this.validateIdle();
|
|
36
|
+
|
|
37
|
+
const pluginName = plugin.name;
|
|
38
|
+
if (this.plugins.has(pluginName)) {
|
|
39
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.plugins.set(pluginName, plugin);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bootstrap the kernel
|
|
48
|
+
* 1. Resolve dependencies (topological sort)
|
|
49
|
+
* 2. Init phase - plugins register services
|
|
50
|
+
* 3. Start phase - plugins execute business logic
|
|
51
|
+
* 4. Trigger 'kernel:ready' hook
|
|
52
|
+
*/
|
|
53
|
+
async bootstrap(): Promise<void> {
|
|
54
|
+
this.validateState('idle');
|
|
55
|
+
|
|
56
|
+
this.state = 'initializing';
|
|
57
|
+
this.logger.info('Bootstrap started');
|
|
58
|
+
|
|
59
|
+
// Resolve dependencies
|
|
60
|
+
const orderedPlugins = this.resolveDependencies();
|
|
61
|
+
|
|
62
|
+
// Phase 1: Init - Plugins register services
|
|
63
|
+
this.logger.info('Phase 1: Init plugins');
|
|
64
|
+
for (const plugin of orderedPlugins) {
|
|
65
|
+
await this.runPluginInit(plugin);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Phase 2: Start - Plugins execute business logic
|
|
69
|
+
this.logger.info('Phase 2: Start plugins');
|
|
70
|
+
this.state = 'running';
|
|
71
|
+
|
|
72
|
+
for (const plugin of orderedPlugins) {
|
|
73
|
+
await this.runPluginStart(plugin);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Trigger ready hook
|
|
77
|
+
await this.triggerHook('kernel:ready');
|
|
78
|
+
this.logger.info('✅ Bootstrap complete', {
|
|
79
|
+
pluginCount: this.plugins.size
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Shutdown the kernel
|
|
85
|
+
* Calls destroy on all plugins in reverse order
|
|
86
|
+
*/
|
|
87
|
+
async shutdown(): Promise<void> {
|
|
88
|
+
await this.destroy();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Graceful shutdown - destroy all plugins in reverse order
|
|
93
|
+
*/
|
|
94
|
+
async destroy(): Promise<void> {
|
|
95
|
+
if (this.state === 'stopped') {
|
|
96
|
+
this.logger.warn('Kernel already stopped');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.state = 'stopping';
|
|
101
|
+
this.logger.info('Shutdown started');
|
|
102
|
+
|
|
103
|
+
// Trigger shutdown hook
|
|
104
|
+
await this.triggerHook('kernel:shutdown');
|
|
105
|
+
|
|
106
|
+
// Destroy plugins in reverse order
|
|
107
|
+
const orderedPlugins = this.resolveDependencies();
|
|
108
|
+
for (const plugin of orderedPlugins.reverse()) {
|
|
109
|
+
await this.runPluginDestroy(plugin);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.state = 'stopped';
|
|
113
|
+
this.logger.info('✅ Shutdown complete');
|
|
114
|
+
|
|
115
|
+
// Cleanup logger resources
|
|
116
|
+
if (this.logger && typeof (this.logger as ObjectLogger).destroy === 'function') {
|
|
117
|
+
await (this.logger as ObjectLogger).destroy();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a service from the registry
|
|
123
|
+
* Convenience method for external access
|
|
124
|
+
*/
|
|
125
|
+
getService<T>(name: string): T {
|
|
126
|
+
return this.context.getService<T>(name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if kernel is running
|
|
131
|
+
*/
|
|
132
|
+
isRunning(): boolean {
|
|
133
|
+
return this.state === 'running';
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/logger.ts
CHANGED
|
@@ -204,7 +204,8 @@ export class ObjectLogger implements Logger {
|
|
|
204
204
|
info: '\x1b[32m', // Green
|
|
205
205
|
warn: '\x1b[33m', // Yellow
|
|
206
206
|
error: '\x1b[31m', // Red
|
|
207
|
-
fatal: '\x1b[35m'
|
|
207
|
+
fatal: '\x1b[35m', // Magenta
|
|
208
|
+
silent: ''
|
|
208
209
|
};
|
|
209
210
|
const reset = '\x1b[0m';
|
|
210
211
|
const color = levelColors[level] || '';
|
|
@@ -263,21 +264,41 @@ export class ObjectLogger implements Logger {
|
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
error(message: string,
|
|
267
|
+
error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {
|
|
268
|
+
let error: Error | undefined;
|
|
269
|
+
let context: Record<string, any> = {};
|
|
270
|
+
|
|
271
|
+
if (errorOrMeta instanceof Error) {
|
|
272
|
+
error = errorOrMeta;
|
|
273
|
+
context = meta || {};
|
|
274
|
+
} else {
|
|
275
|
+
context = errorOrMeta || {};
|
|
276
|
+
}
|
|
277
|
+
|
|
267
278
|
if (this.isNode && this.pinoLogger) {
|
|
268
|
-
const errorContext = error ? { err: error, ...
|
|
279
|
+
const errorContext = error ? { err: error, ...context } : context;
|
|
269
280
|
this.pinoLogger.error(errorContext, message);
|
|
270
281
|
} else {
|
|
271
|
-
this.logBrowser('error', message,
|
|
282
|
+
this.logBrowser('error', message, context, error);
|
|
272
283
|
}
|
|
273
284
|
}
|
|
274
285
|
|
|
275
|
-
fatal(message: string,
|
|
286
|
+
fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {
|
|
287
|
+
let error: Error | undefined;
|
|
288
|
+
let context: Record<string, any> = {};
|
|
289
|
+
|
|
290
|
+
if (errorOrMeta instanceof Error) {
|
|
291
|
+
error = errorOrMeta;
|
|
292
|
+
context = meta || {};
|
|
293
|
+
} else {
|
|
294
|
+
context = errorOrMeta || {};
|
|
295
|
+
}
|
|
296
|
+
|
|
276
297
|
if (this.isNode && this.pinoLogger) {
|
|
277
|
-
const errorContext = error ? { err: error, ...
|
|
298
|
+
const errorContext = error ? { err: error, ...context } : context;
|
|
278
299
|
this.pinoLogger.fatal(errorContext, message);
|
|
279
300
|
} else {
|
|
280
|
-
this.logBrowser('fatal', message,
|
|
301
|
+
this.logBrowser('fatal', message, context, error);
|
|
281
302
|
}
|
|
282
303
|
}
|
|
283
304
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { PluginLoader, ServiceLifecycle, PluginMetadata } from './plugin-loader';
|
|
3
3
|
import { createLogger } from './logger';
|
|
4
|
-
import type { Plugin } from './types';
|
|
4
|
+
import type { Plugin, PluginContext } from './types';
|
|
5
5
|
|
|
6
6
|
describe('PluginLoader', () => {
|
|
7
7
|
let loader: PluginLoader;
|
|
@@ -9,6 +9,15 @@ describe('PluginLoader', () => {
|
|
|
9
9
|
beforeEach(() => {
|
|
10
10
|
const logger = createLogger({ level: 'error' }); // Suppress logs in tests
|
|
11
11
|
loader = new PluginLoader(logger);
|
|
12
|
+
loader.setContext({
|
|
13
|
+
registerService: () => {},
|
|
14
|
+
getService: () => { throw new Error('Mock service not found'); },
|
|
15
|
+
hook: () => {},
|
|
16
|
+
trigger: async () => {},
|
|
17
|
+
getServices: () => new Map(),
|
|
18
|
+
logger: logger,
|
|
19
|
+
getKernel: () => ({}) as any
|
|
20
|
+
} as PluginContext);
|
|
12
21
|
});
|
|
13
22
|
|
|
14
23
|
describe('Plugin Loading', () => {
|
package/src/plugin-loader.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Plugin, PluginContext } from './types.js';
|
|
2
2
|
import type { Logger } from '@objectstack/spec/contracts';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { PluginConfigValidator } from './security/plugin-config-validator.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Service Lifecycle Types
|
|
@@ -111,13 +112,31 @@ export interface VersionCompatibility {
|
|
|
111
112
|
*/
|
|
112
113
|
export class PluginLoader {
|
|
113
114
|
private logger: Logger;
|
|
115
|
+
private context?: PluginContext;
|
|
116
|
+
private configValidator: PluginConfigValidator;
|
|
114
117
|
private loadedPlugins: Map<string, PluginMetadata> = new Map();
|
|
115
118
|
private serviceFactories: Map<string, ServiceRegistration> = new Map();
|
|
116
119
|
private serviceInstances: Map<string, any> = new Map();
|
|
117
120
|
private scopedServices: Map<string, Map<string, any>> = new Map();
|
|
121
|
+
private creating: Set<string> = new Set();
|
|
118
122
|
|
|
119
123
|
constructor(logger: Logger) {
|
|
120
124
|
this.logger = logger;
|
|
125
|
+
this.configValidator = new PluginConfigValidator(logger);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set the plugin context for service factories
|
|
130
|
+
*/
|
|
131
|
+
setContext(context: PluginContext): void {
|
|
132
|
+
this.context = context;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get a synchronous service instance if it exists (Sync Helper)
|
|
137
|
+
*/
|
|
138
|
+
getServiceInstance<T>(name: string): T | undefined {
|
|
139
|
+
return this.serviceInstances.get(name) as T;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
/**
|
|
@@ -371,16 +390,16 @@ export class PluginLoader {
|
|
|
371
390
|
if (!plugin.configSchema) {
|
|
372
391
|
return;
|
|
373
392
|
}
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
393
|
+
|
|
394
|
+
if (config === undefined) {
|
|
395
|
+
// In loadPlugin, we often don't have the config yet.
|
|
396
|
+
// We skip validation here or valid against empty object if schema allows?
|
|
397
|
+
// For now, let's keep the logging behavior but note it's delegating
|
|
398
|
+
this.logger.debug(`Plugin ${plugin.name} has configuration schema (config validation postponed)`);
|
|
399
|
+
return;
|
|
378
400
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// This is a placeholder that logs the validation would happen
|
|
382
|
-
// The actual validation should be done by the caller when config is available
|
|
383
|
-
this.logger.debug(`Plugin ${plugin.name} has configuration schema (use PluginConfigValidator for validation)`);
|
|
401
|
+
|
|
402
|
+
this.configValidator.validatePluginConfig(plugin, config);
|
|
384
403
|
}
|
|
385
404
|
|
|
386
405
|
private async verifyPluginSignature(plugin: PluginMetadata): Promise<void> {
|
|
@@ -431,9 +450,19 @@ export class PluginLoader {
|
|
|
431
450
|
}
|
|
432
451
|
|
|
433
452
|
private async createServiceInstance(registration: ServiceRegistration): Promise<any> {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
453
|
+
if (!this.context) {
|
|
454
|
+
throw new Error(`[PluginLoader] Context not set - cannot create service '${registration.name}'`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (this.creating.has(registration.name)) {
|
|
458
|
+
throw new Error(`Circular dependency detected: ${Array.from(this.creating).join(' -> ')} -> ${registration.name}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.creating.add(registration.name);
|
|
462
|
+
try {
|
|
463
|
+
return await registration.factory(this.context);
|
|
464
|
+
} finally {
|
|
465
|
+
this.creating.delete(registration.name);
|
|
466
|
+
}
|
|
438
467
|
}
|
|
439
468
|
}
|
package/src/security/index.ts
CHANGED
|
@@ -27,3 +27,22 @@ export {
|
|
|
27
27
|
type PluginPermissions,
|
|
28
28
|
type PermissionCheckResult,
|
|
29
29
|
} from './plugin-permission-enforcer.js';
|
|
30
|
+
|
|
31
|
+
// Advanced security components (Phase 2)
|
|
32
|
+
export {
|
|
33
|
+
PluginPermissionManager,
|
|
34
|
+
type PermissionGrant,
|
|
35
|
+
type PermissionCheckResult as PluginPermissionCheckResult,
|
|
36
|
+
} from './permission-manager.js';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
PluginSandboxRuntime,
|
|
40
|
+
type SandboxContext,
|
|
41
|
+
type ResourceUsage,
|
|
42
|
+
} from './sandbox-runtime.js';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
PluginSecurityScanner,
|
|
46
|
+
type ScanTarget,
|
|
47
|
+
type SecurityIssue,
|
|
48
|
+
} from './security-scanner.js';
|