@objectstack/core 1.0.4 → 1.0.6
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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +19 -0
- package/dist/index.cjs +4304 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1777 -0
- package/dist/index.d.ts +1776 -21
- package/dist/index.js +4246 -23
- package/dist/index.js.map +1 -0
- package/package.json +5 -5
- package/src/logger.ts +2 -2
- package/src/security/plugin-signature-verifier.ts +12 -11
- package/tsconfig.json +1 -3
- package/dist/api-registry-plugin.d.ts +0 -54
- package/dist/api-registry-plugin.d.ts.map +0 -1
- package/dist/api-registry-plugin.js +0 -53
- package/dist/api-registry-plugin.test.d.ts +0 -2
- package/dist/api-registry-plugin.test.d.ts.map +0 -1
- package/dist/api-registry-plugin.test.js +0 -334
- package/dist/api-registry.d.ts +0 -259
- package/dist/api-registry.d.ts.map +0 -1
- package/dist/api-registry.js +0 -600
- package/dist/api-registry.test.d.ts +0 -2
- package/dist/api-registry.test.d.ts.map +0 -1
- package/dist/api-registry.test.js +0 -957
- package/dist/contracts/data-engine.d.ts +0 -62
- package/dist/contracts/data-engine.d.ts.map +0 -1
- package/dist/contracts/data-engine.js +0 -1
- package/dist/contracts/http-server.d.ts +0 -119
- package/dist/contracts/http-server.d.ts.map +0 -1
- package/dist/contracts/http-server.js +0 -11
- package/dist/contracts/logger.d.ts +0 -63
- package/dist/contracts/logger.d.ts.map +0 -1
- package/dist/contracts/logger.js +0 -1
- package/dist/dependency-resolver.d.ts +0 -62
- package/dist/dependency-resolver.d.ts.map +0 -1
- package/dist/dependency-resolver.js +0 -317
- package/dist/dependency-resolver.test.d.ts +0 -2
- package/dist/dependency-resolver.test.d.ts.map +0 -1
- package/dist/dependency-resolver.test.js +0 -241
- package/dist/health-monitor.d.ts +0 -65
- package/dist/health-monitor.d.ts.map +0 -1
- package/dist/health-monitor.js +0 -269
- package/dist/health-monitor.test.d.ts +0 -2
- package/dist/health-monitor.test.d.ts.map +0 -1
- package/dist/health-monitor.test.js +0 -68
- package/dist/hot-reload.d.ts +0 -79
- package/dist/hot-reload.d.ts.map +0 -1
- package/dist/hot-reload.js +0 -313
- package/dist/index.d.ts.map +0 -1
- package/dist/kernel-base.d.ts +0 -84
- package/dist/kernel-base.d.ts.map +0 -1
- package/dist/kernel-base.js +0 -219
- package/dist/kernel.d.ts +0 -113
- package/dist/kernel.d.ts.map +0 -1
- package/dist/kernel.js +0 -472
- package/dist/kernel.test.d.ts +0 -2
- package/dist/kernel.test.d.ts.map +0 -1
- package/dist/kernel.test.js +0 -414
- package/dist/lite-kernel.d.ts +0 -55
- package/dist/lite-kernel.d.ts.map +0 -1
- package/dist/lite-kernel.js +0 -112
- package/dist/lite-kernel.test.d.ts +0 -2
- package/dist/lite-kernel.test.d.ts.map +0 -1
- package/dist/lite-kernel.test.js +0 -161
- package/dist/logger.d.ts +0 -71
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -312
- package/dist/logger.test.d.ts +0 -2
- package/dist/logger.test.d.ts.map +0 -1
- package/dist/logger.test.js +0 -92
- package/dist/plugin-loader.d.ts +0 -164
- package/dist/plugin-loader.d.ts.map +0 -1
- package/dist/plugin-loader.js +0 -319
- package/dist/plugin-loader.test.d.ts +0 -2
- package/dist/plugin-loader.test.d.ts.map +0 -1
- package/dist/plugin-loader.test.js +0 -348
- package/dist/qa/adapter.d.ts +0 -14
- package/dist/qa/adapter.d.ts.map +0 -1
- package/dist/qa/adapter.js +0 -1
- package/dist/qa/http-adapter.d.ts +0 -16
- package/dist/qa/http-adapter.d.ts.map +0 -1
- package/dist/qa/http-adapter.js +0 -107
- package/dist/qa/index.d.ts +0 -4
- package/dist/qa/index.d.ts.map +0 -1
- package/dist/qa/index.js +0 -3
- package/dist/qa/runner.d.ts +0 -27
- package/dist/qa/runner.d.ts.map +0 -1
- package/dist/qa/runner.js +0 -157
- package/dist/security/index.d.ts +0 -17
- package/dist/security/index.d.ts.map +0 -1
- package/dist/security/index.js +0 -17
- package/dist/security/permission-manager.d.ts +0 -96
- package/dist/security/permission-manager.d.ts.map +0 -1
- package/dist/security/permission-manager.js +0 -235
- package/dist/security/permission-manager.test.d.ts +0 -2
- package/dist/security/permission-manager.test.d.ts.map +0 -1
- package/dist/security/permission-manager.test.js +0 -220
- package/dist/security/plugin-config-validator.d.ts +0 -79
- package/dist/security/plugin-config-validator.d.ts.map +0 -1
- package/dist/security/plugin-config-validator.js +0 -166
- package/dist/security/plugin-config-validator.test.d.ts +0 -2
- package/dist/security/plugin-config-validator.test.d.ts.map +0 -1
- package/dist/security/plugin-config-validator.test.js +0 -223
- package/dist/security/plugin-permission-enforcer.d.ts +0 -154
- package/dist/security/plugin-permission-enforcer.d.ts.map +0 -1
- package/dist/security/plugin-permission-enforcer.js +0 -323
- package/dist/security/plugin-permission-enforcer.test.d.ts +0 -2
- package/dist/security/plugin-permission-enforcer.test.d.ts.map +0 -1
- package/dist/security/plugin-permission-enforcer.test.js +0 -205
- package/dist/security/plugin-signature-verifier.d.ts +0 -96
- package/dist/security/plugin-signature-verifier.d.ts.map +0 -1
- package/dist/security/plugin-signature-verifier.js +0 -250
- package/dist/security/sandbox-runtime.d.ts +0 -115
- package/dist/security/sandbox-runtime.d.ts.map +0 -1
- package/dist/security/sandbox-runtime.js +0 -311
- package/dist/security/security-scanner.d.ts +0 -92
- package/dist/security/security-scanner.d.ts.map +0 -1
- package/dist/security/security-scanner.js +0 -273
- package/dist/types.d.ts +0 -89
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
- package/dist/utils/env.d.ts +0 -20
- package/dist/utils/env.d.ts.map +0 -1
- package/dist/utils/env.js +0 -46
- package/dist/utils/env.test.d.ts +0 -2
- package/dist/utils/env.test.d.ts.map +0 -1
- package/dist/utils/env.test.js +0 -52
package/dist/index.js
CHANGED
|
@@ -1,23 +1,4246 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/kernel-base.ts
|
|
8
|
+
var ObjectKernelBase = class {
|
|
9
|
+
constructor(logger) {
|
|
10
|
+
this.plugins = /* @__PURE__ */ new Map();
|
|
11
|
+
this.services = /* @__PURE__ */ new Map();
|
|
12
|
+
this.hooks = /* @__PURE__ */ new Map();
|
|
13
|
+
this.state = "idle";
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate kernel state
|
|
18
|
+
* @param requiredState - Required state for the operation
|
|
19
|
+
* @throws Error if current state doesn't match
|
|
20
|
+
*/
|
|
21
|
+
validateState(requiredState) {
|
|
22
|
+
if (this.state !== requiredState) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`[Kernel] Invalid state: expected '${requiredState}', got '${this.state}'`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate kernel is in idle state (for plugin registration)
|
|
30
|
+
*/
|
|
31
|
+
validateIdle() {
|
|
32
|
+
if (this.state !== "idle") {
|
|
33
|
+
throw new Error("[Kernel] Cannot register plugins after bootstrap has started");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create the plugin context
|
|
38
|
+
* Subclasses can override to customize context creation
|
|
39
|
+
*/
|
|
40
|
+
createContext() {
|
|
41
|
+
return {
|
|
42
|
+
registerService: (name, service) => {
|
|
43
|
+
if (this.services instanceof Map) {
|
|
44
|
+
if (this.services.has(name)) {
|
|
45
|
+
throw new Error(`[Kernel] Service '${name}' already registered`);
|
|
46
|
+
}
|
|
47
|
+
this.services.set(name, service);
|
|
48
|
+
} else {
|
|
49
|
+
this.services.register(name, service);
|
|
50
|
+
}
|
|
51
|
+
this.logger.info(`Service '${name}' registered`, { service: name });
|
|
52
|
+
},
|
|
53
|
+
getService: (name) => {
|
|
54
|
+
if (this.services instanceof Map) {
|
|
55
|
+
const service = this.services.get(name);
|
|
56
|
+
if (!service) {
|
|
57
|
+
throw new Error(`[Kernel] Service '${name}' not found`);
|
|
58
|
+
}
|
|
59
|
+
return service;
|
|
60
|
+
} else {
|
|
61
|
+
return this.services.get(name);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
hook: (name, handler) => {
|
|
65
|
+
if (!this.hooks.has(name)) {
|
|
66
|
+
this.hooks.set(name, []);
|
|
67
|
+
}
|
|
68
|
+
this.hooks.get(name).push(handler);
|
|
69
|
+
},
|
|
70
|
+
trigger: async (name, ...args) => {
|
|
71
|
+
const handlers = this.hooks.get(name) || [];
|
|
72
|
+
for (const handler of handlers) {
|
|
73
|
+
await handler(...args);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
getServices: () => {
|
|
77
|
+
if (this.services instanceof Map) {
|
|
78
|
+
return new Map(this.services);
|
|
79
|
+
} else {
|
|
80
|
+
return /* @__PURE__ */ new Map();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
logger: this.logger,
|
|
84
|
+
getKernel: () => this
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve plugin dependencies using topological sort
|
|
89
|
+
* @returns Ordered list of plugins (dependencies first)
|
|
90
|
+
*/
|
|
91
|
+
resolveDependencies() {
|
|
92
|
+
const resolved = [];
|
|
93
|
+
const visited = /* @__PURE__ */ new Set();
|
|
94
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
95
|
+
const visit = (pluginName) => {
|
|
96
|
+
if (visited.has(pluginName)) return;
|
|
97
|
+
if (visiting.has(pluginName)) {
|
|
98
|
+
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
99
|
+
}
|
|
100
|
+
const plugin = this.plugins.get(pluginName);
|
|
101
|
+
if (!plugin) {
|
|
102
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
103
|
+
}
|
|
104
|
+
visiting.add(pluginName);
|
|
105
|
+
const deps = plugin.dependencies || [];
|
|
106
|
+
for (const dep of deps) {
|
|
107
|
+
if (!this.plugins.has(dep)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
visit(dep);
|
|
113
|
+
}
|
|
114
|
+
visiting.delete(pluginName);
|
|
115
|
+
visited.add(pluginName);
|
|
116
|
+
resolved.push(plugin);
|
|
117
|
+
};
|
|
118
|
+
for (const pluginName of this.plugins.keys()) {
|
|
119
|
+
visit(pluginName);
|
|
120
|
+
}
|
|
121
|
+
return resolved;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Run plugin init phase
|
|
125
|
+
* @param plugin - Plugin to initialize
|
|
126
|
+
*/
|
|
127
|
+
async runPluginInit(plugin) {
|
|
128
|
+
const pluginName = plugin.name;
|
|
129
|
+
this.logger.info(`Initializing plugin: ${pluginName}`);
|
|
130
|
+
try {
|
|
131
|
+
await plugin.init(this.context);
|
|
132
|
+
this.logger.info(`Plugin initialized: ${pluginName}`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.logger.error(`Plugin init failed: ${pluginName}`, error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Run plugin start phase
|
|
140
|
+
* @param plugin - Plugin to start
|
|
141
|
+
*/
|
|
142
|
+
async runPluginStart(plugin) {
|
|
143
|
+
if (!plugin.start) return;
|
|
144
|
+
const pluginName = plugin.name;
|
|
145
|
+
this.logger.info(`Starting plugin: ${pluginName}`);
|
|
146
|
+
try {
|
|
147
|
+
await plugin.start(this.context);
|
|
148
|
+
this.logger.info(`Plugin started: ${pluginName}`);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
this.logger.error(`Plugin start failed: ${pluginName}`, error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Run plugin destroy phase
|
|
156
|
+
* @param plugin - Plugin to destroy
|
|
157
|
+
*/
|
|
158
|
+
async runPluginDestroy(plugin) {
|
|
159
|
+
if (!plugin.destroy) return;
|
|
160
|
+
const pluginName = plugin.name;
|
|
161
|
+
this.logger.info(`Destroying plugin: ${pluginName}`);
|
|
162
|
+
try {
|
|
163
|
+
await plugin.destroy();
|
|
164
|
+
this.logger.info(`Plugin destroyed: ${pluginName}`);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.logger.error(`Plugin destroy failed: ${pluginName}`, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Trigger a hook with all registered handlers
|
|
172
|
+
* @param name - Hook name
|
|
173
|
+
* @param args - Arguments to pass to handlers
|
|
174
|
+
*/
|
|
175
|
+
async triggerHook(name, ...args) {
|
|
176
|
+
const handlers = this.hooks.get(name) || [];
|
|
177
|
+
this.logger.debug(`Triggering hook: ${name}`, {
|
|
178
|
+
hook: name,
|
|
179
|
+
handlerCount: handlers.length
|
|
180
|
+
});
|
|
181
|
+
for (const handler of handlers) {
|
|
182
|
+
try {
|
|
183
|
+
await handler(...args);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.logger.error(`Hook handler failed: ${name}`, error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get current kernel state
|
|
191
|
+
*/
|
|
192
|
+
getState() {
|
|
193
|
+
return this.state;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get all registered plugins
|
|
197
|
+
*/
|
|
198
|
+
getPlugins() {
|
|
199
|
+
return new Map(this.plugins);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/utils/env.ts
|
|
204
|
+
var isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
205
|
+
function getEnv(key, defaultValue) {
|
|
206
|
+
if (typeof process !== "undefined" && process.env) {
|
|
207
|
+
return process.env[key] || defaultValue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
if (typeof globalThis !== "undefined" && globalThis.process?.env) {
|
|
211
|
+
return globalThis.process.env[key] || defaultValue;
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
}
|
|
215
|
+
return defaultValue;
|
|
216
|
+
}
|
|
217
|
+
function safeExit(code = 0) {
|
|
218
|
+
if (isNode) {
|
|
219
|
+
process.exit(code);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function getMemoryUsage() {
|
|
223
|
+
if (isNode) {
|
|
224
|
+
return process.memoryUsage();
|
|
225
|
+
}
|
|
226
|
+
return { heapUsed: 0, heapTotal: 0 };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/logger.ts
|
|
230
|
+
var ObjectLogger = class _ObjectLogger {
|
|
231
|
+
// CommonJS require function for Node.js
|
|
232
|
+
constructor(config = {}) {
|
|
233
|
+
this.isNode = isNode;
|
|
234
|
+
this.config = {
|
|
235
|
+
name: config.name,
|
|
236
|
+
level: config.level ?? "info",
|
|
237
|
+
format: config.format ?? (this.isNode ? "json" : "pretty"),
|
|
238
|
+
redact: config.redact ?? ["password", "token", "secret", "key"],
|
|
239
|
+
sourceLocation: config.sourceLocation ?? false,
|
|
240
|
+
file: config.file,
|
|
241
|
+
rotation: config.rotation ?? {
|
|
242
|
+
maxSize: "10m",
|
|
243
|
+
maxFiles: 5
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
if (this.isNode) {
|
|
247
|
+
this.initPinoLogger();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Initialize Pino logger for Node.js
|
|
252
|
+
*/
|
|
253
|
+
async initPinoLogger() {
|
|
254
|
+
if (!this.isNode) return;
|
|
255
|
+
try {
|
|
256
|
+
const { createRequire } = await import("module");
|
|
257
|
+
this.require = createRequire(import.meta.url);
|
|
258
|
+
const pino = this.require("pino");
|
|
259
|
+
const pinoOptions = {
|
|
260
|
+
level: this.config.level,
|
|
261
|
+
redact: {
|
|
262
|
+
paths: this.config.redact,
|
|
263
|
+
censor: "***REDACTED***"
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
if (this.config.name) {
|
|
267
|
+
pinoOptions.name = this.config.name;
|
|
268
|
+
}
|
|
269
|
+
const targets = [];
|
|
270
|
+
if (this.config.format === "pretty") {
|
|
271
|
+
let hasPretty = false;
|
|
272
|
+
try {
|
|
273
|
+
this.require.resolve("pino-pretty");
|
|
274
|
+
hasPretty = true;
|
|
275
|
+
} catch (e) {
|
|
276
|
+
}
|
|
277
|
+
if (hasPretty) {
|
|
278
|
+
targets.push({
|
|
279
|
+
target: "pino-pretty",
|
|
280
|
+
options: {
|
|
281
|
+
colorize: true,
|
|
282
|
+
translateTime: "SYS:standard",
|
|
283
|
+
ignore: "pid,hostname"
|
|
284
|
+
},
|
|
285
|
+
level: this.config.level
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
console.warn("[Logger] pino-pretty not found. Install it for pretty logging: pnpm add -D pino-pretty");
|
|
289
|
+
targets.push({
|
|
290
|
+
target: "pino/file",
|
|
291
|
+
options: { destination: 1 },
|
|
292
|
+
level: this.config.level
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
} else if (this.config.format === "json") {
|
|
296
|
+
targets.push({
|
|
297
|
+
target: "pino/file",
|
|
298
|
+
options: { destination: 1 },
|
|
299
|
+
// stdout
|
|
300
|
+
level: this.config.level
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
targets.push({
|
|
304
|
+
target: "pino/file",
|
|
305
|
+
options: { destination: 1 },
|
|
306
|
+
level: this.config.level
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (this.config.file) {
|
|
310
|
+
targets.push({
|
|
311
|
+
target: "pino/file",
|
|
312
|
+
options: {
|
|
313
|
+
destination: this.config.file,
|
|
314
|
+
mkdir: true
|
|
315
|
+
},
|
|
316
|
+
level: this.config.level
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (targets.length > 0) {
|
|
320
|
+
pinoOptions.transport = targets.length === 1 ? targets[0] : { targets };
|
|
321
|
+
}
|
|
322
|
+
this.pinoInstance = pino(pinoOptions);
|
|
323
|
+
this.pinoLogger = this.pinoInstance;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.warn("[Logger] Pino not available, falling back to console:", error);
|
|
326
|
+
this.pinoLogger = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Redact sensitive keys from context object (for browser)
|
|
331
|
+
*/
|
|
332
|
+
redactSensitive(obj) {
|
|
333
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
334
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
335
|
+
for (const key in redacted) {
|
|
336
|
+
const lowerKey = key.toLowerCase();
|
|
337
|
+
const shouldRedact = this.config.redact.some(
|
|
338
|
+
(pattern) => lowerKey.includes(pattern.toLowerCase())
|
|
339
|
+
);
|
|
340
|
+
if (shouldRedact) {
|
|
341
|
+
redacted[key] = "***REDACTED***";
|
|
342
|
+
} else if (typeof redacted[key] === "object" && redacted[key] !== null) {
|
|
343
|
+
redacted[key] = this.redactSensitive(redacted[key]);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return redacted;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Format log entry for browser
|
|
350
|
+
*/
|
|
351
|
+
formatBrowserLog(level, message, context) {
|
|
352
|
+
if (this.config.format === "json") {
|
|
353
|
+
return JSON.stringify({
|
|
354
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
355
|
+
level,
|
|
356
|
+
message,
|
|
357
|
+
...context
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (this.config.format === "text") {
|
|
361
|
+
const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
|
|
362
|
+
if (context && Object.keys(context).length > 0) {
|
|
363
|
+
parts.push(JSON.stringify(context));
|
|
364
|
+
}
|
|
365
|
+
return parts.join(" | ");
|
|
366
|
+
}
|
|
367
|
+
const levelColors = {
|
|
368
|
+
debug: "\x1B[36m",
|
|
369
|
+
// Cyan
|
|
370
|
+
info: "\x1B[32m",
|
|
371
|
+
// Green
|
|
372
|
+
warn: "\x1B[33m",
|
|
373
|
+
// Yellow
|
|
374
|
+
error: "\x1B[31m",
|
|
375
|
+
// Red
|
|
376
|
+
fatal: "\x1B[35m",
|
|
377
|
+
// Magenta
|
|
378
|
+
silent: ""
|
|
379
|
+
};
|
|
380
|
+
const reset = "\x1B[0m";
|
|
381
|
+
const color = levelColors[level] || "";
|
|
382
|
+
let output = `${color}[${level.toUpperCase()}]${reset} ${message}`;
|
|
383
|
+
if (context && Object.keys(context).length > 0) {
|
|
384
|
+
output += ` ${JSON.stringify(context, null, 2)}`;
|
|
385
|
+
}
|
|
386
|
+
return output;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Log using browser console
|
|
390
|
+
*/
|
|
391
|
+
logBrowser(level, message, context, error) {
|
|
392
|
+
const redactedContext = context ? this.redactSensitive(context) : void 0;
|
|
393
|
+
const mergedContext = error ? { ...redactedContext, error: { message: error.message, stack: error.stack } } : redactedContext;
|
|
394
|
+
const formatted = this.formatBrowserLog(level, message, mergedContext);
|
|
395
|
+
const consoleMethod = level === "debug" ? "debug" : level === "info" ? "log" : level === "warn" ? "warn" : level === "error" || level === "fatal" ? "error" : "log";
|
|
396
|
+
console[consoleMethod](formatted);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Public logging methods
|
|
400
|
+
*/
|
|
401
|
+
debug(message, meta) {
|
|
402
|
+
if (this.isNode && this.pinoLogger) {
|
|
403
|
+
this.pinoLogger.debug(meta || {}, message);
|
|
404
|
+
} else {
|
|
405
|
+
this.logBrowser("debug", message, meta);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
info(message, meta) {
|
|
409
|
+
if (this.isNode && this.pinoLogger) {
|
|
410
|
+
this.pinoLogger.info(meta || {}, message);
|
|
411
|
+
} else {
|
|
412
|
+
this.logBrowser("info", message, meta);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
warn(message, meta) {
|
|
416
|
+
if (this.isNode && this.pinoLogger) {
|
|
417
|
+
this.pinoLogger.warn(meta || {}, message);
|
|
418
|
+
} else {
|
|
419
|
+
this.logBrowser("warn", message, meta);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
error(message, errorOrMeta, meta) {
|
|
423
|
+
let error;
|
|
424
|
+
let context = {};
|
|
425
|
+
if (errorOrMeta instanceof Error) {
|
|
426
|
+
error = errorOrMeta;
|
|
427
|
+
context = meta || {};
|
|
428
|
+
} else {
|
|
429
|
+
context = errorOrMeta || {};
|
|
430
|
+
}
|
|
431
|
+
if (this.isNode && this.pinoLogger) {
|
|
432
|
+
const errorContext = error ? { err: error, ...context } : context;
|
|
433
|
+
this.pinoLogger.error(errorContext, message);
|
|
434
|
+
} else {
|
|
435
|
+
this.logBrowser("error", message, context, error);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
fatal(message, errorOrMeta, meta) {
|
|
439
|
+
let error;
|
|
440
|
+
let context = {};
|
|
441
|
+
if (errorOrMeta instanceof Error) {
|
|
442
|
+
error = errorOrMeta;
|
|
443
|
+
context = meta || {};
|
|
444
|
+
} else {
|
|
445
|
+
context = errorOrMeta || {};
|
|
446
|
+
}
|
|
447
|
+
if (this.isNode && this.pinoLogger) {
|
|
448
|
+
const errorContext = error ? { err: error, ...context } : context;
|
|
449
|
+
this.pinoLogger.fatal(errorContext, message);
|
|
450
|
+
} else {
|
|
451
|
+
this.logBrowser("fatal", message, context, error);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Create a child logger with additional context
|
|
456
|
+
* Note: Child loggers share the parent's Pino instance
|
|
457
|
+
*/
|
|
458
|
+
child(context) {
|
|
459
|
+
const childLogger = new _ObjectLogger(this.config);
|
|
460
|
+
if (this.isNode && this.pinoInstance) {
|
|
461
|
+
childLogger.pinoLogger = this.pinoInstance.child(context);
|
|
462
|
+
childLogger.pinoInstance = this.pinoInstance;
|
|
463
|
+
}
|
|
464
|
+
return childLogger;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Set trace context for distributed tracing
|
|
468
|
+
*/
|
|
469
|
+
withTrace(traceId, spanId) {
|
|
470
|
+
return this.child({ traceId, spanId });
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Cleanup resources
|
|
474
|
+
*/
|
|
475
|
+
async destroy() {
|
|
476
|
+
if (this.pinoLogger && this.pinoLogger.flush) {
|
|
477
|
+
await new Promise((resolve) => {
|
|
478
|
+
this.pinoLogger.flush(() => resolve());
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Compatibility method for console.log usage
|
|
484
|
+
*/
|
|
485
|
+
log(message, ...args) {
|
|
486
|
+
this.info(message, args.length > 0 ? { args } : void 0);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function createLogger(config) {
|
|
490
|
+
return new ObjectLogger(config);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/kernel.ts
|
|
494
|
+
import { ServiceRequirementDef } from "@objectstack/spec/system";
|
|
495
|
+
|
|
496
|
+
// src/security/plugin-config-validator.ts
|
|
497
|
+
import { z } from "zod";
|
|
498
|
+
var PluginConfigValidator = class {
|
|
499
|
+
constructor(logger) {
|
|
500
|
+
this.logger = logger;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Validate plugin configuration against its Zod schema
|
|
504
|
+
*
|
|
505
|
+
* @param plugin - Plugin metadata with configSchema
|
|
506
|
+
* @param config - User-provided configuration
|
|
507
|
+
* @returns Validated and typed configuration
|
|
508
|
+
* @throws Error with detailed validation errors
|
|
509
|
+
*/
|
|
510
|
+
validatePluginConfig(plugin, config) {
|
|
511
|
+
if (!plugin.configSchema) {
|
|
512
|
+
this.logger.debug(`Plugin ${plugin.name} has no config schema - skipping validation`);
|
|
513
|
+
return config;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const validatedConfig = plugin.configSchema.parse(config);
|
|
517
|
+
this.logger.debug(`\u2705 Plugin config validated: ${plugin.name}`, {
|
|
518
|
+
plugin: plugin.name,
|
|
519
|
+
configKeys: Object.keys(config || {}).length
|
|
520
|
+
});
|
|
521
|
+
return validatedConfig;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof z.ZodError) {
|
|
524
|
+
const formattedErrors = this.formatZodErrors(error);
|
|
525
|
+
const errorMessage = [
|
|
526
|
+
`Plugin ${plugin.name} configuration validation failed:`,
|
|
527
|
+
...formattedErrors.map((e) => ` - ${e.path}: ${e.message}`)
|
|
528
|
+
].join("\n");
|
|
529
|
+
this.logger.error(errorMessage, void 0, {
|
|
530
|
+
plugin: plugin.name,
|
|
531
|
+
errors: formattedErrors
|
|
532
|
+
});
|
|
533
|
+
throw new Error(errorMessage);
|
|
534
|
+
}
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Validate partial configuration (for incremental updates)
|
|
540
|
+
*
|
|
541
|
+
* @param plugin - Plugin metadata
|
|
542
|
+
* @param partialConfig - Partial configuration to validate
|
|
543
|
+
* @returns Validated partial configuration
|
|
544
|
+
*/
|
|
545
|
+
validatePartialConfig(plugin, partialConfig) {
|
|
546
|
+
if (!plugin.configSchema) {
|
|
547
|
+
return partialConfig;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const partialSchema = plugin.configSchema.partial();
|
|
551
|
+
const validatedConfig = partialSchema.parse(partialConfig);
|
|
552
|
+
this.logger.debug(`\u2705 Partial config validated: ${plugin.name}`);
|
|
553
|
+
return validatedConfig;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (error instanceof z.ZodError) {
|
|
556
|
+
const formattedErrors = this.formatZodErrors(error);
|
|
557
|
+
const errorMessage = [
|
|
558
|
+
`Plugin ${plugin.name} partial configuration validation failed:`,
|
|
559
|
+
...formattedErrors.map((e) => ` - ${e.path}: ${e.message}`)
|
|
560
|
+
].join("\n");
|
|
561
|
+
throw new Error(errorMessage);
|
|
562
|
+
}
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Get default configuration from schema
|
|
568
|
+
*
|
|
569
|
+
* @param plugin - Plugin metadata
|
|
570
|
+
* @returns Default configuration object
|
|
571
|
+
*/
|
|
572
|
+
getDefaultConfig(plugin) {
|
|
573
|
+
if (!plugin.configSchema) {
|
|
574
|
+
return void 0;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const defaults = plugin.configSchema.parse({});
|
|
578
|
+
this.logger.debug(`Default config extracted: ${plugin.name}`);
|
|
579
|
+
return defaults;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
this.logger.debug(`No default config available: ${plugin.name}`);
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Check if configuration is valid without throwing
|
|
587
|
+
*
|
|
588
|
+
* @param plugin - Plugin metadata
|
|
589
|
+
* @param config - Configuration to check
|
|
590
|
+
* @returns True if valid, false otherwise
|
|
591
|
+
*/
|
|
592
|
+
isConfigValid(plugin, config) {
|
|
593
|
+
if (!plugin.configSchema) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
const result = plugin.configSchema.safeParse(config);
|
|
597
|
+
return result.success;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Get configuration errors without throwing
|
|
601
|
+
*
|
|
602
|
+
* @param plugin - Plugin metadata
|
|
603
|
+
* @param config - Configuration to check
|
|
604
|
+
* @returns Array of validation errors, or empty array if valid
|
|
605
|
+
*/
|
|
606
|
+
getConfigErrors(plugin, config) {
|
|
607
|
+
if (!plugin.configSchema) {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
const result = plugin.configSchema.safeParse(config);
|
|
611
|
+
if (result.success) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
return this.formatZodErrors(result.error);
|
|
615
|
+
}
|
|
616
|
+
// Private methods
|
|
617
|
+
formatZodErrors(error) {
|
|
618
|
+
return error.issues.map((e) => ({
|
|
619
|
+
path: e.path.join(".") || "root",
|
|
620
|
+
message: e.message
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
function createPluginConfigValidator(logger) {
|
|
625
|
+
return new PluginConfigValidator(logger);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/plugin-loader.ts
|
|
629
|
+
var ServiceLifecycle = /* @__PURE__ */ ((ServiceLifecycle2) => {
|
|
630
|
+
ServiceLifecycle2["SINGLETON"] = "singleton";
|
|
631
|
+
ServiceLifecycle2["TRANSIENT"] = "transient";
|
|
632
|
+
ServiceLifecycle2["SCOPED"] = "scoped";
|
|
633
|
+
return ServiceLifecycle2;
|
|
634
|
+
})(ServiceLifecycle || {});
|
|
635
|
+
var PluginLoader = class {
|
|
636
|
+
constructor(logger) {
|
|
637
|
+
this.loadedPlugins = /* @__PURE__ */ new Map();
|
|
638
|
+
this.serviceFactories = /* @__PURE__ */ new Map();
|
|
639
|
+
this.serviceInstances = /* @__PURE__ */ new Map();
|
|
640
|
+
this.scopedServices = /* @__PURE__ */ new Map();
|
|
641
|
+
this.creating = /* @__PURE__ */ new Set();
|
|
642
|
+
this.logger = logger;
|
|
643
|
+
this.configValidator = new PluginConfigValidator(logger);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Set the plugin context for service factories
|
|
647
|
+
*/
|
|
648
|
+
setContext(context) {
|
|
649
|
+
this.context = context;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get a synchronous service instance if it exists (Sync Helper)
|
|
653
|
+
*/
|
|
654
|
+
getServiceInstance(name) {
|
|
655
|
+
return this.serviceInstances.get(name);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Load a plugin asynchronously with validation
|
|
659
|
+
*/
|
|
660
|
+
async loadPlugin(plugin) {
|
|
661
|
+
const startTime = Date.now();
|
|
662
|
+
try {
|
|
663
|
+
this.logger.info(`Loading plugin: ${plugin.name}`);
|
|
664
|
+
const metadata = this.toPluginMetadata(plugin);
|
|
665
|
+
this.validatePluginStructure(metadata);
|
|
666
|
+
const versionCheck = this.checkVersionCompatibility(metadata);
|
|
667
|
+
if (!versionCheck.compatible) {
|
|
668
|
+
throw new Error(`Version incompatible: ${versionCheck.message}`);
|
|
669
|
+
}
|
|
670
|
+
if (metadata.configSchema) {
|
|
671
|
+
this.validatePluginConfig(metadata);
|
|
672
|
+
}
|
|
673
|
+
if (metadata.signature) {
|
|
674
|
+
await this.verifyPluginSignature(metadata);
|
|
675
|
+
}
|
|
676
|
+
this.loadedPlugins.set(metadata.name, metadata);
|
|
677
|
+
const loadTime = Date.now() - startTime;
|
|
678
|
+
this.logger.info(`Plugin loaded: ${plugin.name} (${loadTime}ms)`);
|
|
679
|
+
return {
|
|
680
|
+
success: true,
|
|
681
|
+
plugin: metadata,
|
|
682
|
+
loadTime
|
|
683
|
+
};
|
|
684
|
+
} catch (error) {
|
|
685
|
+
this.logger.error(`Failed to load plugin: ${plugin.name}`, error);
|
|
686
|
+
return {
|
|
687
|
+
success: false,
|
|
688
|
+
error,
|
|
689
|
+
loadTime: Date.now() - startTime
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Register a service with factory function
|
|
695
|
+
*/
|
|
696
|
+
registerServiceFactory(registration) {
|
|
697
|
+
if (this.serviceFactories.has(registration.name)) {
|
|
698
|
+
throw new Error(`Service factory '${registration.name}' already registered`);
|
|
699
|
+
}
|
|
700
|
+
this.serviceFactories.set(registration.name, registration);
|
|
701
|
+
this.logger.debug(`Service factory registered: ${registration.name} (${registration.lifecycle})`);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Get or create a service instance based on lifecycle type
|
|
705
|
+
*/
|
|
706
|
+
async getService(name, scopeId) {
|
|
707
|
+
const registration = this.serviceFactories.get(name);
|
|
708
|
+
if (!registration) {
|
|
709
|
+
const instance = this.serviceInstances.get(name);
|
|
710
|
+
if (!instance) {
|
|
711
|
+
throw new Error(`Service '${name}' not found`);
|
|
712
|
+
}
|
|
713
|
+
return instance;
|
|
714
|
+
}
|
|
715
|
+
switch (registration.lifecycle) {
|
|
716
|
+
case "singleton" /* SINGLETON */:
|
|
717
|
+
return await this.getSingletonService(registration);
|
|
718
|
+
case "transient" /* TRANSIENT */:
|
|
719
|
+
return await this.createTransientService(registration);
|
|
720
|
+
case "scoped" /* SCOPED */:
|
|
721
|
+
if (!scopeId) {
|
|
722
|
+
throw new Error(`Scope ID required for scoped service '${name}'`);
|
|
723
|
+
}
|
|
724
|
+
return await this.getScopedService(registration, scopeId);
|
|
725
|
+
default:
|
|
726
|
+
throw new Error(`Unknown service lifecycle: ${registration.lifecycle}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Register a static service instance (legacy support)
|
|
731
|
+
*/
|
|
732
|
+
registerService(name, service) {
|
|
733
|
+
if (this.serviceInstances.has(name)) {
|
|
734
|
+
throw new Error(`Service '${name}' already registered`);
|
|
735
|
+
}
|
|
736
|
+
this.serviceInstances.set(name, service);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Check if a service is registered (either as instance or factory)
|
|
740
|
+
*/
|
|
741
|
+
hasService(name) {
|
|
742
|
+
return this.serviceInstances.has(name) || this.serviceFactories.has(name);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Detect circular dependencies in service factories
|
|
746
|
+
* Note: This only detects cycles in service dependencies, not plugin dependencies.
|
|
747
|
+
* Plugin dependency cycles are detected in the kernel's resolveDependencies method.
|
|
748
|
+
*/
|
|
749
|
+
detectCircularDependencies() {
|
|
750
|
+
const cycles = [];
|
|
751
|
+
const visited = /* @__PURE__ */ new Set();
|
|
752
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
753
|
+
const visit = (serviceName, path = []) => {
|
|
754
|
+
if (visiting.has(serviceName)) {
|
|
755
|
+
const cycle = [...path, serviceName].join(" -> ");
|
|
756
|
+
cycles.push(cycle);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (visited.has(serviceName)) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
visiting.add(serviceName);
|
|
763
|
+
const registration = this.serviceFactories.get(serviceName);
|
|
764
|
+
if (registration?.dependencies) {
|
|
765
|
+
for (const dep of registration.dependencies) {
|
|
766
|
+
visit(dep, [...path, serviceName]);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
visiting.delete(serviceName);
|
|
770
|
+
visited.add(serviceName);
|
|
771
|
+
};
|
|
772
|
+
for (const serviceName of this.serviceFactories.keys()) {
|
|
773
|
+
visit(serviceName);
|
|
774
|
+
}
|
|
775
|
+
return cycles;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Check plugin health
|
|
779
|
+
*/
|
|
780
|
+
async checkPluginHealth(pluginName) {
|
|
781
|
+
const plugin = this.loadedPlugins.get(pluginName);
|
|
782
|
+
if (!plugin) {
|
|
783
|
+
return {
|
|
784
|
+
healthy: false,
|
|
785
|
+
message: "Plugin not found",
|
|
786
|
+
lastCheck: /* @__PURE__ */ new Date()
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (!plugin.healthCheck) {
|
|
790
|
+
return {
|
|
791
|
+
healthy: true,
|
|
792
|
+
message: "No health check defined",
|
|
793
|
+
lastCheck: /* @__PURE__ */ new Date()
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
const status = await plugin.healthCheck();
|
|
798
|
+
return {
|
|
799
|
+
...status,
|
|
800
|
+
lastCheck: /* @__PURE__ */ new Date()
|
|
801
|
+
};
|
|
802
|
+
} catch (error) {
|
|
803
|
+
return {
|
|
804
|
+
healthy: false,
|
|
805
|
+
message: `Health check failed: ${error.message}`,
|
|
806
|
+
lastCheck: /* @__PURE__ */ new Date()
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Clear scoped services for a scope
|
|
812
|
+
*/
|
|
813
|
+
clearScope(scopeId) {
|
|
814
|
+
this.scopedServices.delete(scopeId);
|
|
815
|
+
this.logger.debug(`Cleared scope: ${scopeId}`);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Get all loaded plugins
|
|
819
|
+
*/
|
|
820
|
+
getLoadedPlugins() {
|
|
821
|
+
return new Map(this.loadedPlugins);
|
|
822
|
+
}
|
|
823
|
+
// Private helper methods
|
|
824
|
+
toPluginMetadata(plugin) {
|
|
825
|
+
return {
|
|
826
|
+
...plugin,
|
|
827
|
+
version: plugin.version || "0.0.0"
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
validatePluginStructure(plugin) {
|
|
831
|
+
if (!plugin.name) {
|
|
832
|
+
throw new Error("Plugin name is required");
|
|
833
|
+
}
|
|
834
|
+
if (!plugin.init) {
|
|
835
|
+
throw new Error("Plugin init function is required");
|
|
836
|
+
}
|
|
837
|
+
if (!this.isValidSemanticVersion(plugin.version)) {
|
|
838
|
+
throw new Error(`Invalid semantic version: ${plugin.version}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
checkVersionCompatibility(plugin) {
|
|
842
|
+
const version = plugin.version;
|
|
843
|
+
if (!this.isValidSemanticVersion(version)) {
|
|
844
|
+
return {
|
|
845
|
+
compatible: false,
|
|
846
|
+
pluginVersion: version,
|
|
847
|
+
message: "Invalid semantic version format"
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
compatible: true,
|
|
852
|
+
pluginVersion: version
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
isValidSemanticVersion(version) {
|
|
856
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
|
|
857
|
+
return semverRegex.test(version);
|
|
858
|
+
}
|
|
859
|
+
validatePluginConfig(plugin, config) {
|
|
860
|
+
if (!plugin.configSchema) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (config === void 0) {
|
|
864
|
+
this.logger.debug(`Plugin ${plugin.name} has configuration schema (config validation postponed)`);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
this.configValidator.validatePluginConfig(plugin, config);
|
|
868
|
+
}
|
|
869
|
+
async verifyPluginSignature(plugin) {
|
|
870
|
+
if (!plugin.signature) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
this.logger.debug(`Plugin ${plugin.name} has signature (use PluginSignatureVerifier for verification)`);
|
|
874
|
+
}
|
|
875
|
+
async getSingletonService(registration) {
|
|
876
|
+
let instance = this.serviceInstances.get(registration.name);
|
|
877
|
+
if (!instance) {
|
|
878
|
+
instance = await this.createServiceInstance(registration);
|
|
879
|
+
this.serviceInstances.set(registration.name, instance);
|
|
880
|
+
this.logger.debug(`Singleton service created: ${registration.name}`);
|
|
881
|
+
}
|
|
882
|
+
return instance;
|
|
883
|
+
}
|
|
884
|
+
async createTransientService(registration) {
|
|
885
|
+
const instance = await this.createServiceInstance(registration);
|
|
886
|
+
this.logger.debug(`Transient service created: ${registration.name}`);
|
|
887
|
+
return instance;
|
|
888
|
+
}
|
|
889
|
+
async getScopedService(registration, scopeId) {
|
|
890
|
+
if (!this.scopedServices.has(scopeId)) {
|
|
891
|
+
this.scopedServices.set(scopeId, /* @__PURE__ */ new Map());
|
|
892
|
+
}
|
|
893
|
+
const scope = this.scopedServices.get(scopeId);
|
|
894
|
+
let instance = scope.get(registration.name);
|
|
895
|
+
if (!instance) {
|
|
896
|
+
instance = await this.createServiceInstance(registration);
|
|
897
|
+
scope.set(registration.name, instance);
|
|
898
|
+
this.logger.debug(`Scoped service created: ${registration.name} (scope: ${scopeId})`);
|
|
899
|
+
}
|
|
900
|
+
return instance;
|
|
901
|
+
}
|
|
902
|
+
async createServiceInstance(registration) {
|
|
903
|
+
if (!this.context) {
|
|
904
|
+
throw new Error(`[PluginLoader] Context not set - cannot create service '${registration.name}'`);
|
|
905
|
+
}
|
|
906
|
+
if (this.creating.has(registration.name)) {
|
|
907
|
+
throw new Error(`Circular dependency detected: ${Array.from(this.creating).join(" -> ")} -> ${registration.name}`);
|
|
908
|
+
}
|
|
909
|
+
this.creating.add(registration.name);
|
|
910
|
+
try {
|
|
911
|
+
return await registration.factory(this.context);
|
|
912
|
+
} finally {
|
|
913
|
+
this.creating.delete(registration.name);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// src/kernel.ts
|
|
919
|
+
var ObjectKernel = class {
|
|
920
|
+
constructor(config = {}) {
|
|
921
|
+
this.plugins = /* @__PURE__ */ new Map();
|
|
922
|
+
this.services = /* @__PURE__ */ new Map();
|
|
923
|
+
this.hooks = /* @__PURE__ */ new Map();
|
|
924
|
+
this.state = "idle";
|
|
925
|
+
this.startedPlugins = /* @__PURE__ */ new Set();
|
|
926
|
+
this.pluginStartTimes = /* @__PURE__ */ new Map();
|
|
927
|
+
this.shutdownHandlers = [];
|
|
928
|
+
this.config = {
|
|
929
|
+
defaultStartupTimeout: 3e4,
|
|
930
|
+
// 30 seconds
|
|
931
|
+
gracefulShutdown: true,
|
|
932
|
+
shutdownTimeout: 6e4,
|
|
933
|
+
// 60 seconds
|
|
934
|
+
rollbackOnFailure: true,
|
|
935
|
+
...config
|
|
936
|
+
};
|
|
937
|
+
this.logger = createLogger(config.logger);
|
|
938
|
+
this.pluginLoader = new PluginLoader(this.logger);
|
|
939
|
+
this.context = {
|
|
940
|
+
registerService: (name, service) => {
|
|
941
|
+
this.registerService(name, service);
|
|
942
|
+
},
|
|
943
|
+
getService: (name) => {
|
|
944
|
+
const service = this.services.get(name);
|
|
945
|
+
if (service) {
|
|
946
|
+
return service;
|
|
947
|
+
}
|
|
948
|
+
const loaderService = this.pluginLoader.getServiceInstance(name);
|
|
949
|
+
if (loaderService) {
|
|
950
|
+
this.services.set(name, loaderService);
|
|
951
|
+
return loaderService;
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
const service2 = this.pluginLoader.getService(name);
|
|
955
|
+
if (service2 instanceof Promise) {
|
|
956
|
+
throw new Error(`Service '${name}' is async - use await`);
|
|
957
|
+
}
|
|
958
|
+
return service2;
|
|
959
|
+
} catch (error) {
|
|
960
|
+
if (error.message?.includes("is async")) {
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
const isNotFoundError = error.message === `Service '${name}' not found`;
|
|
964
|
+
if (!isNotFoundError) {
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
throw new Error(`[Kernel] Service '${name}' not found`);
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
hook: (name, handler) => {
|
|
971
|
+
if (!this.hooks.has(name)) {
|
|
972
|
+
this.hooks.set(name, []);
|
|
973
|
+
}
|
|
974
|
+
this.hooks.get(name).push(handler);
|
|
975
|
+
},
|
|
976
|
+
trigger: async (name, ...args) => {
|
|
977
|
+
const handlers = this.hooks.get(name) || [];
|
|
978
|
+
for (const handler of handlers) {
|
|
979
|
+
await handler(...args);
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
getServices: () => {
|
|
983
|
+
return new Map(this.services);
|
|
984
|
+
},
|
|
985
|
+
logger: this.logger,
|
|
986
|
+
getKernel: () => this
|
|
987
|
+
// Type compatibility
|
|
988
|
+
};
|
|
989
|
+
this.pluginLoader.setContext(this.context);
|
|
990
|
+
if (this.config.gracefulShutdown) {
|
|
991
|
+
this.registerShutdownSignals();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Register a plugin with enhanced validation
|
|
996
|
+
*/
|
|
997
|
+
async use(plugin) {
|
|
998
|
+
if (this.state !== "idle") {
|
|
999
|
+
throw new Error("[Kernel] Cannot register plugins after bootstrap has started");
|
|
1000
|
+
}
|
|
1001
|
+
const result = await this.pluginLoader.loadPlugin(plugin);
|
|
1002
|
+
if (!result.success || !result.plugin) {
|
|
1003
|
+
throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
|
|
1004
|
+
}
|
|
1005
|
+
const pluginMeta = result.plugin;
|
|
1006
|
+
this.plugins.set(pluginMeta.name, pluginMeta);
|
|
1007
|
+
this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
|
|
1008
|
+
plugin: pluginMeta.name,
|
|
1009
|
+
version: pluginMeta.version
|
|
1010
|
+
});
|
|
1011
|
+
return this;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Register a service instance directly
|
|
1015
|
+
*/
|
|
1016
|
+
registerService(name, service) {
|
|
1017
|
+
if (this.services.has(name)) {
|
|
1018
|
+
throw new Error(`[Kernel] Service '${name}' already registered`);
|
|
1019
|
+
}
|
|
1020
|
+
this.services.set(name, service);
|
|
1021
|
+
this.pluginLoader.registerService(name, service);
|
|
1022
|
+
this.logger.info(`Service '${name}' registered`, { service: name });
|
|
1023
|
+
return this;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Register a service factory with lifecycle management
|
|
1027
|
+
*/
|
|
1028
|
+
registerServiceFactory(name, factory, lifecycle = "singleton" /* SINGLETON */, dependencies) {
|
|
1029
|
+
this.pluginLoader.registerServiceFactory({
|
|
1030
|
+
name,
|
|
1031
|
+
factory,
|
|
1032
|
+
lifecycle,
|
|
1033
|
+
dependencies
|
|
1034
|
+
});
|
|
1035
|
+
return this;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Validate Critical System Requirements
|
|
1039
|
+
*/
|
|
1040
|
+
validateSystemRequirements() {
|
|
1041
|
+
if (this.config.skipSystemValidation) {
|
|
1042
|
+
this.logger.debug("System requirement validation skipped");
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
this.logger.debug("Validating system service requirements...");
|
|
1046
|
+
const missingServices = [];
|
|
1047
|
+
const missingCoreServices = [];
|
|
1048
|
+
for (const [serviceName, criticality] of Object.entries(ServiceRequirementDef)) {
|
|
1049
|
+
const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
|
|
1050
|
+
if (!hasService) {
|
|
1051
|
+
if (criticality === "required") {
|
|
1052
|
+
this.logger.error(`CRITICAL: Required service missing: ${serviceName}`);
|
|
1053
|
+
missingServices.push(serviceName);
|
|
1054
|
+
} else if (criticality === "core") {
|
|
1055
|
+
this.logger.warn(`CORE: Core service missing, functionality may be degraded: ${serviceName}`);
|
|
1056
|
+
missingCoreServices.push(serviceName);
|
|
1057
|
+
} else {
|
|
1058
|
+
this.logger.info(`Info: Optional service not present: ${serviceName}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (missingServices.length > 0) {
|
|
1063
|
+
const errorMsg = `System failed to start. Missing critical services: ${missingServices.join(", ")}`;
|
|
1064
|
+
this.logger.error(errorMsg);
|
|
1065
|
+
throw new Error(errorMsg);
|
|
1066
|
+
}
|
|
1067
|
+
if (missingCoreServices.length > 0) {
|
|
1068
|
+
this.logger.warn(`System started with degraded capabilities. Missing core services: ${missingCoreServices.join(", ")}`);
|
|
1069
|
+
}
|
|
1070
|
+
this.logger.info("System requirement check passed");
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Bootstrap the kernel with enhanced features
|
|
1074
|
+
*/
|
|
1075
|
+
async bootstrap() {
|
|
1076
|
+
if (this.state !== "idle") {
|
|
1077
|
+
throw new Error("[Kernel] Kernel already bootstrapped");
|
|
1078
|
+
}
|
|
1079
|
+
this.state = "initializing";
|
|
1080
|
+
this.logger.info("Bootstrap started");
|
|
1081
|
+
try {
|
|
1082
|
+
const cycles = this.pluginLoader.detectCircularDependencies();
|
|
1083
|
+
if (cycles.length > 0) {
|
|
1084
|
+
this.logger.warn("Circular service dependencies detected:", { cycles });
|
|
1085
|
+
}
|
|
1086
|
+
const orderedPlugins = this.resolveDependencies();
|
|
1087
|
+
this.logger.info("Phase 1: Init plugins");
|
|
1088
|
+
for (const plugin of orderedPlugins) {
|
|
1089
|
+
await this.initPluginWithTimeout(plugin);
|
|
1090
|
+
}
|
|
1091
|
+
this.logger.info("Phase 2: Start plugins");
|
|
1092
|
+
this.state = "running";
|
|
1093
|
+
for (const plugin of orderedPlugins) {
|
|
1094
|
+
const result = await this.startPluginWithTimeout(plugin);
|
|
1095
|
+
if (!result.success) {
|
|
1096
|
+
this.logger.error(`Plugin startup failed: ${plugin.name}`, result.error);
|
|
1097
|
+
if (this.config.rollbackOnFailure) {
|
|
1098
|
+
this.logger.warn("Rolling back started plugins...");
|
|
1099
|
+
await this.rollbackStartedPlugins();
|
|
1100
|
+
throw new Error(`Plugin ${plugin.name} failed to start - rollback complete`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
this.validateSystemRequirements();
|
|
1105
|
+
this.logger.debug("Triggering kernel:ready hook");
|
|
1106
|
+
await this.context.trigger("kernel:ready");
|
|
1107
|
+
this.logger.info("\u2705 Bootstrap complete");
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
this.state = "stopped";
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Graceful shutdown with timeout
|
|
1115
|
+
*/
|
|
1116
|
+
async shutdown() {
|
|
1117
|
+
if (this.state === "stopped" || this.state === "stopping") {
|
|
1118
|
+
this.logger.warn("Kernel already stopped or stopping");
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (this.state !== "running") {
|
|
1122
|
+
throw new Error("[Kernel] Kernel not running");
|
|
1123
|
+
}
|
|
1124
|
+
this.state = "stopping";
|
|
1125
|
+
this.logger.info("Graceful shutdown started");
|
|
1126
|
+
try {
|
|
1127
|
+
const shutdownPromise = this.performShutdown();
|
|
1128
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1129
|
+
setTimeout(() => {
|
|
1130
|
+
reject(new Error("Shutdown timeout exceeded"));
|
|
1131
|
+
}, this.config.shutdownTimeout);
|
|
1132
|
+
});
|
|
1133
|
+
await Promise.race([shutdownPromise, timeoutPromise]);
|
|
1134
|
+
this.state = "stopped";
|
|
1135
|
+
this.logger.info("\u2705 Graceful shutdown complete");
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
this.logger.error("Shutdown error - forcing stop", error);
|
|
1138
|
+
this.state = "stopped";
|
|
1139
|
+
throw error;
|
|
1140
|
+
} finally {
|
|
1141
|
+
await this.logger.destroy();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Check health of a specific plugin
|
|
1146
|
+
*/
|
|
1147
|
+
async checkPluginHealth(pluginName) {
|
|
1148
|
+
return await this.pluginLoader.checkPluginHealth(pluginName);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Check health of all plugins
|
|
1152
|
+
*/
|
|
1153
|
+
async checkAllPluginsHealth() {
|
|
1154
|
+
const results = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const pluginName of this.plugins.keys()) {
|
|
1156
|
+
const health = await this.checkPluginHealth(pluginName);
|
|
1157
|
+
results.set(pluginName, health);
|
|
1158
|
+
}
|
|
1159
|
+
return results;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Get plugin startup metrics
|
|
1163
|
+
*/
|
|
1164
|
+
getPluginMetrics() {
|
|
1165
|
+
return new Map(this.pluginStartTimes);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Get a service (sync helper)
|
|
1169
|
+
*/
|
|
1170
|
+
getService(name) {
|
|
1171
|
+
return this.context.getService(name);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Get a service asynchronously (supports factories)
|
|
1175
|
+
*/
|
|
1176
|
+
async getServiceAsync(name, scopeId) {
|
|
1177
|
+
return await this.pluginLoader.getService(name, scopeId);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Check if kernel is running
|
|
1181
|
+
*/
|
|
1182
|
+
isRunning() {
|
|
1183
|
+
return this.state === "running";
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Get kernel state
|
|
1187
|
+
*/
|
|
1188
|
+
getState() {
|
|
1189
|
+
return this.state;
|
|
1190
|
+
}
|
|
1191
|
+
// Private methods
|
|
1192
|
+
async initPluginWithTimeout(plugin) {
|
|
1193
|
+
const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
|
|
1194
|
+
this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
|
|
1195
|
+
const initPromise = plugin.init(this.context);
|
|
1196
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1197
|
+
setTimeout(() => {
|
|
1198
|
+
reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
|
|
1199
|
+
}, timeout);
|
|
1200
|
+
});
|
|
1201
|
+
await Promise.race([initPromise, timeoutPromise]);
|
|
1202
|
+
}
|
|
1203
|
+
async startPluginWithTimeout(plugin) {
|
|
1204
|
+
if (!plugin.start) {
|
|
1205
|
+
return { success: true, pluginName: plugin.name };
|
|
1206
|
+
}
|
|
1207
|
+
const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout;
|
|
1208
|
+
const startTime = Date.now();
|
|
1209
|
+
this.logger.debug(`Start: ${plugin.name}`, { plugin: plugin.name });
|
|
1210
|
+
try {
|
|
1211
|
+
const startPromise = plugin.start(this.context);
|
|
1212
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1213
|
+
setTimeout(() => {
|
|
1214
|
+
reject(new Error(`Plugin ${plugin.name} start timeout after ${timeout}ms`));
|
|
1215
|
+
}, timeout);
|
|
1216
|
+
});
|
|
1217
|
+
await Promise.race([startPromise, timeoutPromise]);
|
|
1218
|
+
const duration = Date.now() - startTime;
|
|
1219
|
+
this.startedPlugins.add(plugin.name);
|
|
1220
|
+
this.pluginStartTimes.set(plugin.name, duration);
|
|
1221
|
+
this.logger.debug(`Plugin started: ${plugin.name} (${duration}ms)`);
|
|
1222
|
+
return {
|
|
1223
|
+
success: true,
|
|
1224
|
+
pluginName: plugin.name,
|
|
1225
|
+
startTime: duration
|
|
1226
|
+
};
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
const duration = Date.now() - startTime;
|
|
1229
|
+
const isTimeout = error.message.includes("timeout");
|
|
1230
|
+
return {
|
|
1231
|
+
success: false,
|
|
1232
|
+
pluginName: plugin.name,
|
|
1233
|
+
error,
|
|
1234
|
+
startTime: duration,
|
|
1235
|
+
timedOut: isTimeout
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
async rollbackStartedPlugins() {
|
|
1240
|
+
const pluginsToRollback = Array.from(this.startedPlugins).reverse();
|
|
1241
|
+
for (const pluginName of pluginsToRollback) {
|
|
1242
|
+
const plugin = this.plugins.get(pluginName);
|
|
1243
|
+
if (plugin?.destroy) {
|
|
1244
|
+
try {
|
|
1245
|
+
this.logger.debug(`Rollback: ${pluginName}`);
|
|
1246
|
+
await plugin.destroy();
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
this.logger.error(`Rollback failed for ${pluginName}`, error);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
this.startedPlugins.clear();
|
|
1253
|
+
}
|
|
1254
|
+
async performShutdown() {
|
|
1255
|
+
await this.context.trigger("kernel:shutdown");
|
|
1256
|
+
const orderedPlugins = Array.from(this.plugins.values()).reverse();
|
|
1257
|
+
for (const plugin of orderedPlugins) {
|
|
1258
|
+
if (plugin.destroy) {
|
|
1259
|
+
this.logger.debug(`Destroy: ${plugin.name}`, { plugin: plugin.name });
|
|
1260
|
+
try {
|
|
1261
|
+
await plugin.destroy();
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
this.logger.error(`Error destroying plugin ${plugin.name}`, error);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const handler of this.shutdownHandlers) {
|
|
1268
|
+
try {
|
|
1269
|
+
await handler();
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
this.logger.error("Shutdown handler error", error);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
resolveDependencies() {
|
|
1276
|
+
const resolved = [];
|
|
1277
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1278
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1279
|
+
const visit = (pluginName) => {
|
|
1280
|
+
if (visited.has(pluginName)) return;
|
|
1281
|
+
if (visiting.has(pluginName)) {
|
|
1282
|
+
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
1283
|
+
}
|
|
1284
|
+
const plugin = this.plugins.get(pluginName);
|
|
1285
|
+
if (!plugin) {
|
|
1286
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
1287
|
+
}
|
|
1288
|
+
visiting.add(pluginName);
|
|
1289
|
+
const deps = plugin.dependencies || [];
|
|
1290
|
+
for (const dep of deps) {
|
|
1291
|
+
if (!this.plugins.has(dep)) {
|
|
1292
|
+
throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
|
|
1293
|
+
}
|
|
1294
|
+
visit(dep);
|
|
1295
|
+
}
|
|
1296
|
+
visiting.delete(pluginName);
|
|
1297
|
+
visited.add(pluginName);
|
|
1298
|
+
resolved.push(plugin);
|
|
1299
|
+
};
|
|
1300
|
+
for (const pluginName of this.plugins.keys()) {
|
|
1301
|
+
visit(pluginName);
|
|
1302
|
+
}
|
|
1303
|
+
return resolved;
|
|
1304
|
+
}
|
|
1305
|
+
registerShutdownSignals() {
|
|
1306
|
+
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
1307
|
+
let shutdownInProgress = false;
|
|
1308
|
+
const handleShutdown = async (signal) => {
|
|
1309
|
+
if (shutdownInProgress) {
|
|
1310
|
+
this.logger.warn(`Shutdown already in progress, ignoring ${signal}`);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
shutdownInProgress = true;
|
|
1314
|
+
this.logger.info(`Received ${signal} - initiating graceful shutdown`);
|
|
1315
|
+
try {
|
|
1316
|
+
await this.shutdown();
|
|
1317
|
+
safeExit(0);
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
this.logger.error("Shutdown failed", error);
|
|
1320
|
+
safeExit(1);
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
if (isNode) {
|
|
1324
|
+
for (const signal of signals) {
|
|
1325
|
+
process.on(signal, () => handleShutdown(signal));
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Register a custom shutdown handler
|
|
1331
|
+
*/
|
|
1332
|
+
onShutdown(handler) {
|
|
1333
|
+
this.shutdownHandlers.push(handler);
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
// src/lite-kernel.ts
|
|
1338
|
+
var LiteKernel = class extends ObjectKernelBase {
|
|
1339
|
+
constructor(config) {
|
|
1340
|
+
const logger = createLogger(config?.logger);
|
|
1341
|
+
super(logger);
|
|
1342
|
+
this.context = this.createContext();
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Register a plugin
|
|
1346
|
+
* @param plugin - Plugin instance
|
|
1347
|
+
*/
|
|
1348
|
+
use(plugin) {
|
|
1349
|
+
this.validateIdle();
|
|
1350
|
+
const pluginName = plugin.name;
|
|
1351
|
+
if (this.plugins.has(pluginName)) {
|
|
1352
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
|
|
1353
|
+
}
|
|
1354
|
+
this.plugins.set(pluginName, plugin);
|
|
1355
|
+
return this;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Bootstrap the kernel
|
|
1359
|
+
* 1. Resolve dependencies (topological sort)
|
|
1360
|
+
* 2. Init phase - plugins register services
|
|
1361
|
+
* 3. Start phase - plugins execute business logic
|
|
1362
|
+
* 4. Trigger 'kernel:ready' hook
|
|
1363
|
+
*/
|
|
1364
|
+
async bootstrap() {
|
|
1365
|
+
this.validateState("idle");
|
|
1366
|
+
this.state = "initializing";
|
|
1367
|
+
this.logger.info("Bootstrap started");
|
|
1368
|
+
const orderedPlugins = this.resolveDependencies();
|
|
1369
|
+
this.logger.info("Phase 1: Init plugins");
|
|
1370
|
+
for (const plugin of orderedPlugins) {
|
|
1371
|
+
await this.runPluginInit(plugin);
|
|
1372
|
+
}
|
|
1373
|
+
this.logger.info("Phase 2: Start plugins");
|
|
1374
|
+
this.state = "running";
|
|
1375
|
+
for (const plugin of orderedPlugins) {
|
|
1376
|
+
await this.runPluginStart(plugin);
|
|
1377
|
+
}
|
|
1378
|
+
await this.triggerHook("kernel:ready");
|
|
1379
|
+
this.logger.info("\u2705 Bootstrap complete", {
|
|
1380
|
+
pluginCount: this.plugins.size
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Shutdown the kernel
|
|
1385
|
+
* Calls destroy on all plugins in reverse order
|
|
1386
|
+
*/
|
|
1387
|
+
async shutdown() {
|
|
1388
|
+
await this.destroy();
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Graceful shutdown - destroy all plugins in reverse order
|
|
1392
|
+
*/
|
|
1393
|
+
async destroy() {
|
|
1394
|
+
if (this.state === "stopped") {
|
|
1395
|
+
this.logger.warn("Kernel already stopped");
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
this.state = "stopping";
|
|
1399
|
+
this.logger.info("Shutdown started");
|
|
1400
|
+
await this.triggerHook("kernel:shutdown");
|
|
1401
|
+
const orderedPlugins = this.resolveDependencies();
|
|
1402
|
+
for (const plugin of orderedPlugins.reverse()) {
|
|
1403
|
+
await this.runPluginDestroy(plugin);
|
|
1404
|
+
}
|
|
1405
|
+
this.state = "stopped";
|
|
1406
|
+
this.logger.info("\u2705 Shutdown complete");
|
|
1407
|
+
if (this.logger && typeof this.logger.destroy === "function") {
|
|
1408
|
+
await this.logger.destroy();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Get a service from the registry
|
|
1413
|
+
* Convenience method for external access
|
|
1414
|
+
*/
|
|
1415
|
+
getService(name) {
|
|
1416
|
+
return this.context.getService(name);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Check if kernel is running
|
|
1420
|
+
*/
|
|
1421
|
+
isRunning() {
|
|
1422
|
+
return this.state === "running";
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/api-registry.ts
|
|
1427
|
+
import { ApiRegistryEntrySchema } from "@objectstack/spec/api";
|
|
1428
|
+
var ApiRegistry = class {
|
|
1429
|
+
constructor(logger, conflictResolution = "error", version = "1.0.0") {
|
|
1430
|
+
this.apis = /* @__PURE__ */ new Map();
|
|
1431
|
+
this.endpoints = /* @__PURE__ */ new Map();
|
|
1432
|
+
this.routes = /* @__PURE__ */ new Map();
|
|
1433
|
+
// Performance optimization: Auxiliary indices for O(1) lookups
|
|
1434
|
+
this.apisByType = /* @__PURE__ */ new Map();
|
|
1435
|
+
this.apisByTag = /* @__PURE__ */ new Map();
|
|
1436
|
+
this.apisByStatus = /* @__PURE__ */ new Map();
|
|
1437
|
+
this.logger = logger;
|
|
1438
|
+
this.conflictResolution = conflictResolution;
|
|
1439
|
+
this.version = version;
|
|
1440
|
+
this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Register an API with its endpoints
|
|
1444
|
+
*
|
|
1445
|
+
* @param api - API registry entry
|
|
1446
|
+
* @throws Error if API already registered or route conflicts detected
|
|
1447
|
+
*/
|
|
1448
|
+
registerApi(api) {
|
|
1449
|
+
if (this.apis.has(api.id)) {
|
|
1450
|
+
throw new Error(`[ApiRegistry] API '${api.id}' already registered`);
|
|
1451
|
+
}
|
|
1452
|
+
const fullApi = ApiRegistryEntrySchema.parse(api);
|
|
1453
|
+
for (const endpoint of fullApi.endpoints) {
|
|
1454
|
+
this.validateEndpoint(endpoint, fullApi.id);
|
|
1455
|
+
}
|
|
1456
|
+
this.apis.set(fullApi.id, fullApi);
|
|
1457
|
+
for (const endpoint of fullApi.endpoints) {
|
|
1458
|
+
this.registerEndpoint(fullApi.id, endpoint);
|
|
1459
|
+
}
|
|
1460
|
+
this.updateIndices(fullApi);
|
|
1461
|
+
this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1462
|
+
this.logger.info(`API registered: ${fullApi.id}`, {
|
|
1463
|
+
api: fullApi.id,
|
|
1464
|
+
type: fullApi.type,
|
|
1465
|
+
endpointCount: fullApi.endpoints.length
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Unregister an API and all its endpoints
|
|
1470
|
+
*
|
|
1471
|
+
* @param apiId - API identifier
|
|
1472
|
+
*/
|
|
1473
|
+
unregisterApi(apiId) {
|
|
1474
|
+
const api = this.apis.get(apiId);
|
|
1475
|
+
if (!api) {
|
|
1476
|
+
throw new Error(`[ApiRegistry] API '${apiId}' not found`);
|
|
1477
|
+
}
|
|
1478
|
+
for (const endpoint of api.endpoints) {
|
|
1479
|
+
this.unregisterEndpoint(apiId, endpoint.id);
|
|
1480
|
+
}
|
|
1481
|
+
this.removeFromIndices(api);
|
|
1482
|
+
this.apis.delete(apiId);
|
|
1483
|
+
this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1484
|
+
this.logger.info(`API unregistered: ${apiId}`);
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Register a single endpoint
|
|
1488
|
+
*
|
|
1489
|
+
* @param apiId - API identifier
|
|
1490
|
+
* @param endpoint - Endpoint registration
|
|
1491
|
+
* @throws Error if route conflict detected
|
|
1492
|
+
*/
|
|
1493
|
+
registerEndpoint(apiId, endpoint) {
|
|
1494
|
+
const endpointKey = `${apiId}:${endpoint.id}`;
|
|
1495
|
+
if (this.endpoints.has(endpointKey)) {
|
|
1496
|
+
throw new Error(`[ApiRegistry] Endpoint '${endpoint.id}' already registered for API '${apiId}'`);
|
|
1497
|
+
}
|
|
1498
|
+
this.endpoints.set(endpointKey, { api: apiId, endpoint });
|
|
1499
|
+
if (endpoint.path) {
|
|
1500
|
+
this.registerRoute(apiId, endpoint);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Unregister a single endpoint
|
|
1505
|
+
*
|
|
1506
|
+
* @param apiId - API identifier
|
|
1507
|
+
* @param endpointId - Endpoint identifier
|
|
1508
|
+
*/
|
|
1509
|
+
unregisterEndpoint(apiId, endpointId) {
|
|
1510
|
+
const endpointKey = `${apiId}:${endpointId}`;
|
|
1511
|
+
const entry = this.endpoints.get(endpointKey);
|
|
1512
|
+
if (!entry) {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if (entry.endpoint.path) {
|
|
1516
|
+
const routeKey = this.getRouteKey(entry.endpoint);
|
|
1517
|
+
this.routes.delete(routeKey);
|
|
1518
|
+
}
|
|
1519
|
+
this.endpoints.delete(endpointKey);
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Register a route with conflict detection
|
|
1523
|
+
*
|
|
1524
|
+
* @param apiId - API identifier
|
|
1525
|
+
* @param endpoint - Endpoint registration
|
|
1526
|
+
* @throws Error if route conflict detected (based on strategy)
|
|
1527
|
+
*/
|
|
1528
|
+
registerRoute(apiId, endpoint) {
|
|
1529
|
+
const routeKey = this.getRouteKey(endpoint);
|
|
1530
|
+
const priority = endpoint.priority ?? 100;
|
|
1531
|
+
const existingRoute = this.routes.get(routeKey);
|
|
1532
|
+
if (existingRoute) {
|
|
1533
|
+
this.handleRouteConflict(routeKey, apiId, endpoint, existingRoute, priority);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
this.routes.set(routeKey, {
|
|
1537
|
+
api: apiId,
|
|
1538
|
+
endpointId: endpoint.id,
|
|
1539
|
+
priority
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Handle route conflict based on resolution strategy
|
|
1544
|
+
*
|
|
1545
|
+
* @param routeKey - Route key
|
|
1546
|
+
* @param apiId - New API identifier
|
|
1547
|
+
* @param endpoint - New endpoint
|
|
1548
|
+
* @param existingRoute - Existing route registration
|
|
1549
|
+
* @param newPriority - New endpoint priority
|
|
1550
|
+
* @throws Error if strategy is 'error'
|
|
1551
|
+
*/
|
|
1552
|
+
handleRouteConflict(routeKey, apiId, endpoint, existingRoute, newPriority) {
|
|
1553
|
+
const strategy = this.conflictResolution;
|
|
1554
|
+
switch (strategy) {
|
|
1555
|
+
case "error":
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
`[ApiRegistry] Route conflict detected: '${routeKey}' is already registered by API '${existingRoute.api}' endpoint '${existingRoute.endpointId}'`
|
|
1558
|
+
);
|
|
1559
|
+
case "priority":
|
|
1560
|
+
if (newPriority > existingRoute.priority) {
|
|
1561
|
+
this.logger.warn(
|
|
1562
|
+
`Route conflict: replacing '${routeKey}' (priority ${existingRoute.priority} -> ${newPriority})`,
|
|
1563
|
+
{
|
|
1564
|
+
oldApi: existingRoute.api,
|
|
1565
|
+
oldEndpoint: existingRoute.endpointId,
|
|
1566
|
+
newApi: apiId,
|
|
1567
|
+
newEndpoint: endpoint.id
|
|
1568
|
+
}
|
|
1569
|
+
);
|
|
1570
|
+
this.routes.set(routeKey, {
|
|
1571
|
+
api: apiId,
|
|
1572
|
+
endpointId: endpoint.id,
|
|
1573
|
+
priority: newPriority
|
|
1574
|
+
});
|
|
1575
|
+
} else {
|
|
1576
|
+
this.logger.warn(
|
|
1577
|
+
`Route conflict: keeping existing '${routeKey}' (priority ${existingRoute.priority} >= ${newPriority})`,
|
|
1578
|
+
{
|
|
1579
|
+
existingApi: existingRoute.api,
|
|
1580
|
+
existingEndpoint: existingRoute.endpointId,
|
|
1581
|
+
newApi: apiId,
|
|
1582
|
+
newEndpoint: endpoint.id
|
|
1583
|
+
}
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
break;
|
|
1587
|
+
case "first-wins":
|
|
1588
|
+
this.logger.warn(
|
|
1589
|
+
`Route conflict: keeping first registered '${routeKey}'`,
|
|
1590
|
+
{
|
|
1591
|
+
existingApi: existingRoute.api,
|
|
1592
|
+
newApi: apiId
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
break;
|
|
1596
|
+
case "last-wins":
|
|
1597
|
+
this.logger.warn(
|
|
1598
|
+
`Route conflict: replacing with last registered '${routeKey}'`,
|
|
1599
|
+
{
|
|
1600
|
+
oldApi: existingRoute.api,
|
|
1601
|
+
newApi: apiId
|
|
1602
|
+
}
|
|
1603
|
+
);
|
|
1604
|
+
this.routes.set(routeKey, {
|
|
1605
|
+
api: apiId,
|
|
1606
|
+
endpointId: endpoint.id,
|
|
1607
|
+
priority: newPriority
|
|
1608
|
+
});
|
|
1609
|
+
break;
|
|
1610
|
+
default:
|
|
1611
|
+
throw new Error(`[ApiRegistry] Unknown conflict resolution strategy: ${strategy}`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Generate a unique route key for conflict detection
|
|
1616
|
+
*
|
|
1617
|
+
* NOTE: This implementation uses exact string matching for route conflict detection.
|
|
1618
|
+
* It works well for static paths but has limitations with parameterized routes.
|
|
1619
|
+
* For example, `/api/users/:id` and `/api/users/:userId` will NOT be detected as conflicts
|
|
1620
|
+
* even though they are semantically identical parameterized patterns. Similarly,
|
|
1621
|
+
* `/api/:resource/list` and `/api/:entity/list` would also not be detected as conflicting.
|
|
1622
|
+
*
|
|
1623
|
+
* For more advanced conflict detection (e.g., path-to-regexp pattern matching),
|
|
1624
|
+
* consider integrating with your routing library's conflict detection mechanism.
|
|
1625
|
+
*
|
|
1626
|
+
* @param endpoint - Endpoint registration
|
|
1627
|
+
* @returns Route key (e.g., "GET:/api/v1/customers/:id")
|
|
1628
|
+
*/
|
|
1629
|
+
getRouteKey(endpoint) {
|
|
1630
|
+
const method = endpoint.method || "ANY";
|
|
1631
|
+
return `${method}:${endpoint.path}`;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Validate endpoint registration
|
|
1635
|
+
*
|
|
1636
|
+
* @param endpoint - Endpoint to validate
|
|
1637
|
+
* @param apiId - API identifier (for error messages)
|
|
1638
|
+
* @throws Error if endpoint is invalid
|
|
1639
|
+
*/
|
|
1640
|
+
validateEndpoint(endpoint, apiId) {
|
|
1641
|
+
if (!endpoint.id) {
|
|
1642
|
+
throw new Error(`[ApiRegistry] Endpoint in API '${apiId}' missing 'id' field`);
|
|
1643
|
+
}
|
|
1644
|
+
if (!endpoint.path) {
|
|
1645
|
+
throw new Error(`[ApiRegistry] Endpoint '${endpoint.id}' in API '${apiId}' missing 'path' field`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Get an API by ID
|
|
1650
|
+
*
|
|
1651
|
+
* @param apiId - API identifier
|
|
1652
|
+
* @returns API registry entry or undefined
|
|
1653
|
+
*/
|
|
1654
|
+
getApi(apiId) {
|
|
1655
|
+
return this.apis.get(apiId);
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Get all registered APIs
|
|
1659
|
+
*
|
|
1660
|
+
* @returns Array of all APIs
|
|
1661
|
+
*/
|
|
1662
|
+
getAllApis() {
|
|
1663
|
+
return Array.from(this.apis.values());
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Find APIs matching query criteria
|
|
1667
|
+
*
|
|
1668
|
+
* Performance optimized with auxiliary indices for O(1) lookups on type, tags, and status.
|
|
1669
|
+
*
|
|
1670
|
+
* @param query - Discovery query parameters
|
|
1671
|
+
* @returns Matching APIs
|
|
1672
|
+
*/
|
|
1673
|
+
findApis(query) {
|
|
1674
|
+
let resultIds;
|
|
1675
|
+
if (query.type) {
|
|
1676
|
+
const typeIds = this.apisByType.get(query.type);
|
|
1677
|
+
if (!typeIds || typeIds.size === 0) {
|
|
1678
|
+
return { apis: [], total: 0, filters: query };
|
|
1679
|
+
}
|
|
1680
|
+
resultIds = new Set(typeIds);
|
|
1681
|
+
}
|
|
1682
|
+
if (query.status) {
|
|
1683
|
+
const statusIds = this.apisByStatus.get(query.status);
|
|
1684
|
+
if (!statusIds || statusIds.size === 0) {
|
|
1685
|
+
return { apis: [], total: 0, filters: query };
|
|
1686
|
+
}
|
|
1687
|
+
if (resultIds) {
|
|
1688
|
+
resultIds = new Set([...resultIds].filter((id) => statusIds.has(id)));
|
|
1689
|
+
} else {
|
|
1690
|
+
resultIds = new Set(statusIds);
|
|
1691
|
+
}
|
|
1692
|
+
if (resultIds.size === 0) {
|
|
1693
|
+
return { apis: [], total: 0, filters: query };
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (query.tags && query.tags.length > 0) {
|
|
1697
|
+
const tagMatches = /* @__PURE__ */ new Set();
|
|
1698
|
+
for (const tag of query.tags) {
|
|
1699
|
+
const tagIds = this.apisByTag.get(tag);
|
|
1700
|
+
if (tagIds) {
|
|
1701
|
+
tagIds.forEach((id) => tagMatches.add(id));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (tagMatches.size === 0) {
|
|
1705
|
+
return { apis: [], total: 0, filters: query };
|
|
1706
|
+
}
|
|
1707
|
+
if (resultIds) {
|
|
1708
|
+
resultIds = new Set([...resultIds].filter((id) => tagMatches.has(id)));
|
|
1709
|
+
} else {
|
|
1710
|
+
resultIds = tagMatches;
|
|
1711
|
+
}
|
|
1712
|
+
if (resultIds.size === 0) {
|
|
1713
|
+
return { apis: [], total: 0, filters: query };
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
let results;
|
|
1717
|
+
if (resultIds) {
|
|
1718
|
+
results = Array.from(resultIds).map((id) => this.apis.get(id)).filter((api) => api !== void 0);
|
|
1719
|
+
} else {
|
|
1720
|
+
results = Array.from(this.apis.values());
|
|
1721
|
+
}
|
|
1722
|
+
if (query.pluginSource) {
|
|
1723
|
+
results = results.filter(
|
|
1724
|
+
(api) => api.metadata?.pluginSource === query.pluginSource
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
if (query.version) {
|
|
1728
|
+
results = results.filter((api) => api.version === query.version);
|
|
1729
|
+
}
|
|
1730
|
+
if (query.search) {
|
|
1731
|
+
const searchLower = query.search.toLowerCase();
|
|
1732
|
+
results = results.filter(
|
|
1733
|
+
(api) => api.name.toLowerCase().includes(searchLower) || api.description && api.description.toLowerCase().includes(searchLower)
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
return {
|
|
1737
|
+
apis: results,
|
|
1738
|
+
total: results.length,
|
|
1739
|
+
filters: query
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Get endpoint by API ID and endpoint ID
|
|
1744
|
+
*
|
|
1745
|
+
* @param apiId - API identifier
|
|
1746
|
+
* @param endpointId - Endpoint identifier
|
|
1747
|
+
* @returns Endpoint registration or undefined
|
|
1748
|
+
*/
|
|
1749
|
+
getEndpoint(apiId, endpointId) {
|
|
1750
|
+
const key = `${apiId}:${endpointId}`;
|
|
1751
|
+
return this.endpoints.get(key)?.endpoint;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Find endpoint by route (method + path)
|
|
1755
|
+
*
|
|
1756
|
+
* @param method - HTTP method
|
|
1757
|
+
* @param path - URL path
|
|
1758
|
+
* @returns Endpoint registration or undefined
|
|
1759
|
+
*/
|
|
1760
|
+
findEndpointByRoute(method, path) {
|
|
1761
|
+
const routeKey = `${method}:${path}`;
|
|
1762
|
+
const route = this.routes.get(routeKey);
|
|
1763
|
+
if (!route) {
|
|
1764
|
+
return void 0;
|
|
1765
|
+
}
|
|
1766
|
+
const api = this.apis.get(route.api);
|
|
1767
|
+
const endpoint = this.getEndpoint(route.api, route.endpointId);
|
|
1768
|
+
if (!api || !endpoint) {
|
|
1769
|
+
return void 0;
|
|
1770
|
+
}
|
|
1771
|
+
return { api, endpoint };
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Get complete registry snapshot
|
|
1775
|
+
*
|
|
1776
|
+
* @returns Current registry state
|
|
1777
|
+
*/
|
|
1778
|
+
getRegistry() {
|
|
1779
|
+
const apis = Array.from(this.apis.values());
|
|
1780
|
+
const byType = {};
|
|
1781
|
+
for (const api of apis) {
|
|
1782
|
+
if (!byType[api.type]) {
|
|
1783
|
+
byType[api.type] = [];
|
|
1784
|
+
}
|
|
1785
|
+
byType[api.type].push(api);
|
|
1786
|
+
}
|
|
1787
|
+
const byStatus = {};
|
|
1788
|
+
for (const api of apis) {
|
|
1789
|
+
const status = api.metadata?.status || "active";
|
|
1790
|
+
if (!byStatus[status]) {
|
|
1791
|
+
byStatus[status] = [];
|
|
1792
|
+
}
|
|
1793
|
+
byStatus[status].push(api);
|
|
1794
|
+
}
|
|
1795
|
+
const totalEndpoints = apis.reduce(
|
|
1796
|
+
(sum, api) => sum + api.endpoints.length,
|
|
1797
|
+
0
|
|
1798
|
+
);
|
|
1799
|
+
return {
|
|
1800
|
+
version: this.version,
|
|
1801
|
+
conflictResolution: this.conflictResolution,
|
|
1802
|
+
apis,
|
|
1803
|
+
totalApis: apis.length,
|
|
1804
|
+
totalEndpoints,
|
|
1805
|
+
byType,
|
|
1806
|
+
byStatus,
|
|
1807
|
+
updatedAt: this.updatedAt
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Clear all registered APIs
|
|
1812
|
+
*
|
|
1813
|
+
* **⚠️ SAFETY WARNING:**
|
|
1814
|
+
* This method clears all registered APIs and should be used with caution.
|
|
1815
|
+
*
|
|
1816
|
+
* **Usage Restrictions:**
|
|
1817
|
+
* - In production environments (NODE_ENV=production), a `force: true` parameter is required
|
|
1818
|
+
* - Primarily intended for testing and development hot-reload scenarios
|
|
1819
|
+
*
|
|
1820
|
+
* @param options - Clear options
|
|
1821
|
+
* @param options.force - Force clear in production environment (default: false)
|
|
1822
|
+
* @throws Error if called in production without force flag
|
|
1823
|
+
*
|
|
1824
|
+
* @example Safe usage in tests
|
|
1825
|
+
* ```typescript
|
|
1826
|
+
* beforeEach(() => {
|
|
1827
|
+
* registry.clear(); // OK in test environment
|
|
1828
|
+
* });
|
|
1829
|
+
* ```
|
|
1830
|
+
*
|
|
1831
|
+
* @example Usage in production (requires explicit force)
|
|
1832
|
+
* ```typescript
|
|
1833
|
+
* // In production, explicit force is required
|
|
1834
|
+
* registry.clear({ force: true });
|
|
1835
|
+
* ```
|
|
1836
|
+
*/
|
|
1837
|
+
clear(options = {}) {
|
|
1838
|
+
const isProduction = this.isProductionEnvironment();
|
|
1839
|
+
if (isProduction && !options.force) {
|
|
1840
|
+
throw new Error(
|
|
1841
|
+
"[ApiRegistry] Cannot clear registry in production environment without force flag. Use clear({ force: true }) if you really want to clear the registry."
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
this.apis.clear();
|
|
1845
|
+
this.endpoints.clear();
|
|
1846
|
+
this.routes.clear();
|
|
1847
|
+
this.apisByType.clear();
|
|
1848
|
+
this.apisByTag.clear();
|
|
1849
|
+
this.apisByStatus.clear();
|
|
1850
|
+
this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1851
|
+
if (isProduction) {
|
|
1852
|
+
this.logger.warn("API registry forcefully cleared in production", { force: options.force });
|
|
1853
|
+
} else {
|
|
1854
|
+
this.logger.info("API registry cleared");
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Get registry statistics
|
|
1859
|
+
*
|
|
1860
|
+
* @returns Registry statistics
|
|
1861
|
+
*/
|
|
1862
|
+
getStats() {
|
|
1863
|
+
const apis = Array.from(this.apis.values());
|
|
1864
|
+
const apisByType = {};
|
|
1865
|
+
for (const api of apis) {
|
|
1866
|
+
apisByType[api.type] = (apisByType[api.type] || 0) + 1;
|
|
1867
|
+
}
|
|
1868
|
+
const endpointsByApi = {};
|
|
1869
|
+
for (const api of apis) {
|
|
1870
|
+
endpointsByApi[api.id] = api.endpoints.length;
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
totalApis: this.apis.size,
|
|
1874
|
+
totalEndpoints: this.endpoints.size,
|
|
1875
|
+
totalRoutes: this.routes.size,
|
|
1876
|
+
apisByType,
|
|
1877
|
+
endpointsByApi
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Update auxiliary indices when an API is registered
|
|
1882
|
+
*
|
|
1883
|
+
* @param api - API entry to index
|
|
1884
|
+
* @private
|
|
1885
|
+
* @internal
|
|
1886
|
+
*/
|
|
1887
|
+
updateIndices(api) {
|
|
1888
|
+
this.ensureIndexSet(this.apisByType, api.type).add(api.id);
|
|
1889
|
+
const status = api.metadata?.status || "active";
|
|
1890
|
+
this.ensureIndexSet(this.apisByStatus, status).add(api.id);
|
|
1891
|
+
const tags = api.metadata?.tags || [];
|
|
1892
|
+
for (const tag of tags) {
|
|
1893
|
+
this.ensureIndexSet(this.apisByTag, tag).add(api.id);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Remove API from auxiliary indices when unregistered
|
|
1898
|
+
*
|
|
1899
|
+
* @param api - API entry to remove from indices
|
|
1900
|
+
* @private
|
|
1901
|
+
* @internal
|
|
1902
|
+
*/
|
|
1903
|
+
removeFromIndices(api) {
|
|
1904
|
+
this.removeFromIndexSet(this.apisByType, api.type, api.id);
|
|
1905
|
+
const status = api.metadata?.status || "active";
|
|
1906
|
+
this.removeFromIndexSet(this.apisByStatus, status, api.id);
|
|
1907
|
+
const tags = api.metadata?.tags || [];
|
|
1908
|
+
for (const tag of tags) {
|
|
1909
|
+
this.removeFromIndexSet(this.apisByTag, tag, api.id);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Helper to ensure an index set exists and return it
|
|
1914
|
+
*
|
|
1915
|
+
* @param map - Index map
|
|
1916
|
+
* @param key - Index key
|
|
1917
|
+
* @returns The Set for this key (created if needed)
|
|
1918
|
+
* @private
|
|
1919
|
+
* @internal
|
|
1920
|
+
*/
|
|
1921
|
+
ensureIndexSet(map, key) {
|
|
1922
|
+
let set = map.get(key);
|
|
1923
|
+
if (!set) {
|
|
1924
|
+
set = /* @__PURE__ */ new Set();
|
|
1925
|
+
map.set(key, set);
|
|
1926
|
+
}
|
|
1927
|
+
return set;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Helper to remove an ID from an index set and clean up empty sets
|
|
1931
|
+
*
|
|
1932
|
+
* @param map - Index map
|
|
1933
|
+
* @param key - Index key
|
|
1934
|
+
* @param id - API ID to remove
|
|
1935
|
+
* @private
|
|
1936
|
+
* @internal
|
|
1937
|
+
*/
|
|
1938
|
+
removeFromIndexSet(map, key, id) {
|
|
1939
|
+
const set = map.get(key);
|
|
1940
|
+
if (set) {
|
|
1941
|
+
set.delete(id);
|
|
1942
|
+
if (set.size === 0) {
|
|
1943
|
+
map.delete(key);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Check if running in production environment
|
|
1949
|
+
*
|
|
1950
|
+
* @returns true if NODE_ENV is 'production'
|
|
1951
|
+
* @private
|
|
1952
|
+
* @internal
|
|
1953
|
+
*/
|
|
1954
|
+
isProductionEnvironment() {
|
|
1955
|
+
return getEnv("NODE_ENV") === "production";
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
// src/api-registry-plugin.ts
|
|
1960
|
+
function createApiRegistryPlugin(config = {}) {
|
|
1961
|
+
const {
|
|
1962
|
+
conflictResolution = "error",
|
|
1963
|
+
version = "1.0.0"
|
|
1964
|
+
} = config;
|
|
1965
|
+
return {
|
|
1966
|
+
name: "com.objectstack.core.api-registry",
|
|
1967
|
+
version: "1.0.0",
|
|
1968
|
+
init: async (ctx) => {
|
|
1969
|
+
const registry = new ApiRegistry(
|
|
1970
|
+
ctx.logger,
|
|
1971
|
+
conflictResolution,
|
|
1972
|
+
version
|
|
1973
|
+
);
|
|
1974
|
+
ctx.registerService("api-registry", registry);
|
|
1975
|
+
ctx.logger.info("API Registry plugin initialized", {
|
|
1976
|
+
conflictResolution,
|
|
1977
|
+
version
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/qa/index.ts
|
|
1984
|
+
var qa_exports = {};
|
|
1985
|
+
__export(qa_exports, {
|
|
1986
|
+
HttpTestAdapter: () => HttpTestAdapter,
|
|
1987
|
+
TestRunner: () => TestRunner
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// src/qa/runner.ts
|
|
1991
|
+
var TestRunner = class {
|
|
1992
|
+
constructor(adapter) {
|
|
1993
|
+
this.adapter = adapter;
|
|
1994
|
+
}
|
|
1995
|
+
async runSuite(suite) {
|
|
1996
|
+
const results = [];
|
|
1997
|
+
for (const scenario of suite.scenarios) {
|
|
1998
|
+
results.push(await this.runScenario(scenario));
|
|
1999
|
+
}
|
|
2000
|
+
return results;
|
|
2001
|
+
}
|
|
2002
|
+
async runScenario(scenario) {
|
|
2003
|
+
const startTime = Date.now();
|
|
2004
|
+
const context = {};
|
|
2005
|
+
if (scenario.setup) {
|
|
2006
|
+
for (const step of scenario.setup) {
|
|
2007
|
+
try {
|
|
2008
|
+
await this.runStep(step, context);
|
|
2009
|
+
} catch (e) {
|
|
2010
|
+
return {
|
|
2011
|
+
scenarioId: scenario.id,
|
|
2012
|
+
passed: false,
|
|
2013
|
+
steps: [],
|
|
2014
|
+
error: `Setup failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2015
|
+
duration: Date.now() - startTime
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const stepResults = [];
|
|
2021
|
+
let scenarioPassed = true;
|
|
2022
|
+
let scenarioError = void 0;
|
|
2023
|
+
for (const step of scenario.steps) {
|
|
2024
|
+
const stepStartTime = Date.now();
|
|
2025
|
+
try {
|
|
2026
|
+
const output = await this.runStep(step, context);
|
|
2027
|
+
stepResults.push({
|
|
2028
|
+
stepName: step.name,
|
|
2029
|
+
passed: true,
|
|
2030
|
+
output,
|
|
2031
|
+
duration: Date.now() - stepStartTime
|
|
2032
|
+
});
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
scenarioPassed = false;
|
|
2035
|
+
scenarioError = e;
|
|
2036
|
+
stepResults.push({
|
|
2037
|
+
stepName: step.name,
|
|
2038
|
+
passed: false,
|
|
2039
|
+
error: e,
|
|
2040
|
+
duration: Date.now() - stepStartTime
|
|
2041
|
+
});
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (scenario.teardown) {
|
|
2046
|
+
for (const step of scenario.teardown) {
|
|
2047
|
+
try {
|
|
2048
|
+
await this.runStep(step, context);
|
|
2049
|
+
} catch (e) {
|
|
2050
|
+
if (scenarioPassed) {
|
|
2051
|
+
scenarioPassed = false;
|
|
2052
|
+
scenarioError = `Teardown failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return {
|
|
2058
|
+
scenarioId: scenario.id,
|
|
2059
|
+
passed: scenarioPassed,
|
|
2060
|
+
steps: stepResults,
|
|
2061
|
+
error: scenarioError,
|
|
2062
|
+
duration: Date.now() - startTime
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
async runStep(step, context) {
|
|
2066
|
+
const resolvedAction = this.resolveVariables(step.action, context);
|
|
2067
|
+
const result = await this.adapter.execute(resolvedAction, context);
|
|
2068
|
+
if (step.capture) {
|
|
2069
|
+
for (const [varName, path] of Object.entries(step.capture)) {
|
|
2070
|
+
context[varName] = this.getValueByPath(result, path);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
if (step.assertions) {
|
|
2074
|
+
for (const assertion of step.assertions) {
|
|
2075
|
+
this.assert(result, assertion, context);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return result;
|
|
2079
|
+
}
|
|
2080
|
+
resolveVariables(action, _context) {
|
|
2081
|
+
return action;
|
|
2082
|
+
}
|
|
2083
|
+
getValueByPath(obj, path) {
|
|
2084
|
+
if (!path) return obj;
|
|
2085
|
+
const parts = path.split(".");
|
|
2086
|
+
let current = obj;
|
|
2087
|
+
for (const part of parts) {
|
|
2088
|
+
if (current === null || current === void 0) return void 0;
|
|
2089
|
+
current = current[part];
|
|
2090
|
+
}
|
|
2091
|
+
return current;
|
|
2092
|
+
}
|
|
2093
|
+
assert(result, assertion, _context) {
|
|
2094
|
+
const actual = this.getValueByPath(result, assertion.field);
|
|
2095
|
+
const expected = assertion.expectedValue;
|
|
2096
|
+
switch (assertion.operator) {
|
|
2097
|
+
case "equals":
|
|
2098
|
+
if (actual !== expected) throw new Error(`Assertion failed: ${assertion.field} expected ${expected}, got ${actual}`);
|
|
2099
|
+
break;
|
|
2100
|
+
case "not_equals":
|
|
2101
|
+
if (actual === expected) throw new Error(`Assertion failed: ${assertion.field} expected not ${expected}, got ${actual}`);
|
|
2102
|
+
break;
|
|
2103
|
+
case "contains":
|
|
2104
|
+
if (Array.isArray(actual)) {
|
|
2105
|
+
if (!actual.includes(expected)) throw new Error(`Assertion failed: ${assertion.field} array does not contain ${expected}`);
|
|
2106
|
+
} else if (typeof actual === "string") {
|
|
2107
|
+
if (!actual.includes(String(expected))) throw new Error(`Assertion failed: ${assertion.field} string does not contain ${expected}`);
|
|
2108
|
+
}
|
|
2109
|
+
break;
|
|
2110
|
+
case "not_null":
|
|
2111
|
+
if (actual === null || actual === void 0) throw new Error(`Assertion failed: ${assertion.field} is null`);
|
|
2112
|
+
break;
|
|
2113
|
+
case "is_null":
|
|
2114
|
+
if (actual !== null && actual !== void 0) throw new Error(`Assertion failed: ${assertion.field} is not null`);
|
|
2115
|
+
break;
|
|
2116
|
+
// ... Add other operators
|
|
2117
|
+
default:
|
|
2118
|
+
throw new Error(`Unknown assertion operator: ${assertion.operator}`);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
// src/qa/http-adapter.ts
|
|
2124
|
+
var HttpTestAdapter = class {
|
|
2125
|
+
constructor(baseUrl, authToken) {
|
|
2126
|
+
this.baseUrl = baseUrl;
|
|
2127
|
+
this.authToken = authToken;
|
|
2128
|
+
}
|
|
2129
|
+
async execute(action, _context) {
|
|
2130
|
+
const headers = {
|
|
2131
|
+
"Content-Type": "application/json"
|
|
2132
|
+
};
|
|
2133
|
+
if (this.authToken) {
|
|
2134
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
2135
|
+
}
|
|
2136
|
+
if (action.user) {
|
|
2137
|
+
headers["X-Run-As"] = action.user;
|
|
2138
|
+
}
|
|
2139
|
+
switch (action.type) {
|
|
2140
|
+
case "create_record":
|
|
2141
|
+
return this.createRecord(action.target, action.payload || {}, headers);
|
|
2142
|
+
case "update_record":
|
|
2143
|
+
return this.updateRecord(action.target, action.payload || {}, headers);
|
|
2144
|
+
case "delete_record":
|
|
2145
|
+
return this.deleteRecord(action.target, action.payload || {}, headers);
|
|
2146
|
+
case "read_record":
|
|
2147
|
+
return this.readRecord(action.target, action.payload || {}, headers);
|
|
2148
|
+
case "query_records":
|
|
2149
|
+
return this.queryRecords(action.target, action.payload || {}, headers);
|
|
2150
|
+
case "api_call":
|
|
2151
|
+
return this.rawApiCall(action.target, action.payload || {}, headers);
|
|
2152
|
+
case "wait":
|
|
2153
|
+
const ms = Number(action.payload?.duration || 1e3);
|
|
2154
|
+
return new Promise((resolve) => setTimeout(() => resolve({ waited: ms }), ms));
|
|
2155
|
+
default:
|
|
2156
|
+
throw new Error(`Unsupported action type in HttpAdapter: ${action.type}`);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
async createRecord(objectName, data, headers) {
|
|
2160
|
+
const response = await fetch(`${this.baseUrl}/api/data/${objectName}`, {
|
|
2161
|
+
method: "POST",
|
|
2162
|
+
headers,
|
|
2163
|
+
body: JSON.stringify(data)
|
|
2164
|
+
});
|
|
2165
|
+
return this.handleResponse(response);
|
|
2166
|
+
}
|
|
2167
|
+
async updateRecord(objectName, data, headers) {
|
|
2168
|
+
const id = data._id || data.id;
|
|
2169
|
+
if (!id) throw new Error("Update record requires _id or id in payload");
|
|
2170
|
+
const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
|
|
2171
|
+
method: "PUT",
|
|
2172
|
+
headers,
|
|
2173
|
+
body: JSON.stringify(data)
|
|
2174
|
+
});
|
|
2175
|
+
return this.handleResponse(response);
|
|
2176
|
+
}
|
|
2177
|
+
async deleteRecord(objectName, data, headers) {
|
|
2178
|
+
const id = data._id || data.id;
|
|
2179
|
+
if (!id) throw new Error("Delete record requires _id or id in payload");
|
|
2180
|
+
const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
|
|
2181
|
+
method: "DELETE",
|
|
2182
|
+
headers
|
|
2183
|
+
});
|
|
2184
|
+
return this.handleResponse(response);
|
|
2185
|
+
}
|
|
2186
|
+
async readRecord(objectName, data, headers) {
|
|
2187
|
+
const id = data._id || data.id;
|
|
2188
|
+
if (!id) throw new Error("Read record requires _id or id in payload");
|
|
2189
|
+
const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
|
|
2190
|
+
method: "GET",
|
|
2191
|
+
headers
|
|
2192
|
+
});
|
|
2193
|
+
return this.handleResponse(response);
|
|
2194
|
+
}
|
|
2195
|
+
async queryRecords(objectName, data, headers) {
|
|
2196
|
+
const response = await fetch(`${this.baseUrl}/api/data/${objectName}/query`, {
|
|
2197
|
+
method: "POST",
|
|
2198
|
+
headers,
|
|
2199
|
+
body: JSON.stringify(data)
|
|
2200
|
+
});
|
|
2201
|
+
return this.handleResponse(response);
|
|
2202
|
+
}
|
|
2203
|
+
async rawApiCall(endpoint, data, headers) {
|
|
2204
|
+
const method = data.method || "GET";
|
|
2205
|
+
const body = data.body ? JSON.stringify(data.body) : void 0;
|
|
2206
|
+
const url = endpoint.startsWith("http") ? endpoint : `${this.baseUrl}${endpoint}`;
|
|
2207
|
+
const response = await fetch(url, {
|
|
2208
|
+
method,
|
|
2209
|
+
headers,
|
|
2210
|
+
body
|
|
2211
|
+
});
|
|
2212
|
+
return this.handleResponse(response);
|
|
2213
|
+
}
|
|
2214
|
+
async handleResponse(response) {
|
|
2215
|
+
if (!response.ok) {
|
|
2216
|
+
const text = await response.text();
|
|
2217
|
+
throw new Error(`HTTP Error ${response.status}: ${text}`);
|
|
2218
|
+
}
|
|
2219
|
+
const contentType = response.headers.get("content-type");
|
|
2220
|
+
if (contentType && contentType.includes("application/json")) {
|
|
2221
|
+
return response.json();
|
|
2222
|
+
}
|
|
2223
|
+
return response.text();
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
// src/security/plugin-signature-verifier.ts
|
|
2228
|
+
var cryptoModule = null;
|
|
2229
|
+
var PluginSignatureVerifier = class {
|
|
2230
|
+
constructor(config, logger) {
|
|
2231
|
+
this.config = config;
|
|
2232
|
+
this.logger = logger;
|
|
2233
|
+
this.validateConfig();
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Verify plugin signature
|
|
2237
|
+
*
|
|
2238
|
+
* @param plugin - Plugin metadata with signature
|
|
2239
|
+
* @returns Verification result
|
|
2240
|
+
* @throws Error if verification fails in strict mode
|
|
2241
|
+
*/
|
|
2242
|
+
async verifyPluginSignature(plugin) {
|
|
2243
|
+
if (!plugin.signature) {
|
|
2244
|
+
return this.handleUnsignedPlugin(plugin);
|
|
2245
|
+
}
|
|
2246
|
+
try {
|
|
2247
|
+
const publisherId = this.extractPublisherId(plugin.name);
|
|
2248
|
+
const publicKey = this.config.trustedPublicKeys.get(publisherId);
|
|
2249
|
+
if (!publicKey) {
|
|
2250
|
+
const error = `No trusted public key for publisher: ${publisherId}`;
|
|
2251
|
+
this.logger.warn(error, { plugin: plugin.name, publisherId });
|
|
2252
|
+
if (this.config.strictMode && !this.config.allowSelfSigned) {
|
|
2253
|
+
throw new Error(error);
|
|
2254
|
+
}
|
|
2255
|
+
return {
|
|
2256
|
+
verified: false,
|
|
2257
|
+
error,
|
|
2258
|
+
publisherId
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
const pluginHash = this.computePluginHash(plugin);
|
|
2262
|
+
const isValid = await this.verifyCryptoSignature(
|
|
2263
|
+
pluginHash,
|
|
2264
|
+
plugin.signature,
|
|
2265
|
+
publicKey
|
|
2266
|
+
);
|
|
2267
|
+
if (!isValid) {
|
|
2268
|
+
const error = `Signature verification failed for plugin: ${plugin.name}`;
|
|
2269
|
+
this.logger.error(error, void 0, { plugin: plugin.name, publisherId });
|
|
2270
|
+
throw new Error(error);
|
|
2271
|
+
}
|
|
2272
|
+
this.logger.info(`\u2705 Plugin signature verified: ${plugin.name}`, {
|
|
2273
|
+
plugin: plugin.name,
|
|
2274
|
+
publisherId,
|
|
2275
|
+
algorithm: this.config.algorithm
|
|
2276
|
+
});
|
|
2277
|
+
return {
|
|
2278
|
+
verified: true,
|
|
2279
|
+
publisherId,
|
|
2280
|
+
algorithm: this.config.algorithm
|
|
2281
|
+
};
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
this.logger.error(`Signature verification error: ${plugin.name}`, error);
|
|
2284
|
+
if (this.config.strictMode) {
|
|
2285
|
+
throw error;
|
|
2286
|
+
}
|
|
2287
|
+
return {
|
|
2288
|
+
verified: false,
|
|
2289
|
+
error: error.message
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Register a trusted public key for a publisher
|
|
2295
|
+
*/
|
|
2296
|
+
registerPublicKey(publisherId, publicKey) {
|
|
2297
|
+
this.config.trustedPublicKeys.set(publisherId, publicKey);
|
|
2298
|
+
this.logger.info(`Trusted public key registered for: ${publisherId}`);
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Remove a trusted public key
|
|
2302
|
+
*/
|
|
2303
|
+
revokePublicKey(publisherId) {
|
|
2304
|
+
this.config.trustedPublicKeys.delete(publisherId);
|
|
2305
|
+
this.logger.warn(`Public key revoked for: ${publisherId}`);
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Get list of trusted publishers
|
|
2309
|
+
*/
|
|
2310
|
+
getTrustedPublishers() {
|
|
2311
|
+
return Array.from(this.config.trustedPublicKeys.keys());
|
|
2312
|
+
}
|
|
2313
|
+
// Private methods
|
|
2314
|
+
handleUnsignedPlugin(plugin) {
|
|
2315
|
+
if (this.config.strictMode) {
|
|
2316
|
+
const error = `Plugin missing signature (strict mode): ${plugin.name}`;
|
|
2317
|
+
this.logger.error(error, void 0, { plugin: plugin.name });
|
|
2318
|
+
throw new Error(error);
|
|
2319
|
+
}
|
|
2320
|
+
this.logger.warn(`\u26A0\uFE0F Plugin not signed: ${plugin.name}`, {
|
|
2321
|
+
plugin: plugin.name,
|
|
2322
|
+
recommendation: "Consider signing plugins for production environments"
|
|
2323
|
+
});
|
|
2324
|
+
return {
|
|
2325
|
+
verified: false,
|
|
2326
|
+
error: "Plugin not signed"
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
extractPublisherId(pluginName) {
|
|
2330
|
+
const parts = pluginName.split(".");
|
|
2331
|
+
if (parts.length < 2) {
|
|
2332
|
+
throw new Error(`Invalid plugin name format: ${pluginName} (expected reverse domain notation)`);
|
|
2333
|
+
}
|
|
2334
|
+
return `${parts[0]}.${parts[1]}`;
|
|
2335
|
+
}
|
|
2336
|
+
computePluginHash(plugin) {
|
|
2337
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2338
|
+
return this.computePluginHashBrowser(plugin);
|
|
2339
|
+
}
|
|
2340
|
+
return this.computePluginHashNode(plugin);
|
|
2341
|
+
}
|
|
2342
|
+
computePluginHashNode(plugin) {
|
|
2343
|
+
if (!cryptoModule) {
|
|
2344
|
+
this.logger.warn("crypto module not available, using fallback hash");
|
|
2345
|
+
return this.computePluginHashFallback(plugin);
|
|
2346
|
+
}
|
|
2347
|
+
const pluginCode = this.serializePluginCode(plugin);
|
|
2348
|
+
return cryptoModule.createHash("sha256").update(pluginCode).digest("hex");
|
|
2349
|
+
}
|
|
2350
|
+
computePluginHashBrowser(plugin) {
|
|
2351
|
+
this.logger.debug("Using browser hash (SubtleCrypto integration pending)");
|
|
2352
|
+
return this.computePluginHashFallback(plugin);
|
|
2353
|
+
}
|
|
2354
|
+
computePluginHashFallback(plugin) {
|
|
2355
|
+
const pluginCode = this.serializePluginCode(plugin);
|
|
2356
|
+
let hash = 0;
|
|
2357
|
+
for (let i = 0; i < pluginCode.length; i++) {
|
|
2358
|
+
const char = pluginCode.charCodeAt(i);
|
|
2359
|
+
hash = (hash << 5) - hash + char;
|
|
2360
|
+
hash = hash & hash;
|
|
2361
|
+
}
|
|
2362
|
+
return hash.toString(16);
|
|
2363
|
+
}
|
|
2364
|
+
serializePluginCode(plugin) {
|
|
2365
|
+
const parts = [
|
|
2366
|
+
plugin.name,
|
|
2367
|
+
plugin.version,
|
|
2368
|
+
plugin.init.toString()
|
|
2369
|
+
];
|
|
2370
|
+
if (plugin.start) {
|
|
2371
|
+
parts.push(plugin.start.toString());
|
|
2372
|
+
}
|
|
2373
|
+
if (plugin.destroy) {
|
|
2374
|
+
parts.push(plugin.destroy.toString());
|
|
2375
|
+
}
|
|
2376
|
+
return parts.join("|");
|
|
2377
|
+
}
|
|
2378
|
+
async verifyCryptoSignature(data, signature, publicKey) {
|
|
2379
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2380
|
+
return this.verifyCryptoSignatureBrowser(data, signature, publicKey);
|
|
2381
|
+
}
|
|
2382
|
+
return this.verifyCryptoSignatureNode(data, signature, publicKey);
|
|
2383
|
+
}
|
|
2384
|
+
async verifyCryptoSignatureNode(data, signature, publicKey) {
|
|
2385
|
+
if (!cryptoModule) {
|
|
2386
|
+
try {
|
|
2387
|
+
cryptoModule = await import("crypto");
|
|
2388
|
+
} catch (e) {
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
if (!cryptoModule) {
|
|
2392
|
+
this.logger.error("Crypto module not available for signature verification");
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
try {
|
|
2396
|
+
if (this.config.algorithm === "ES256") {
|
|
2397
|
+
const verify = cryptoModule.createVerify("sha256");
|
|
2398
|
+
verify.update(data);
|
|
2399
|
+
return verify.verify(
|
|
2400
|
+
{
|
|
2401
|
+
key: publicKey,
|
|
2402
|
+
format: "pem",
|
|
2403
|
+
type: "spki"
|
|
2404
|
+
},
|
|
2405
|
+
signature,
|
|
2406
|
+
"base64"
|
|
2407
|
+
);
|
|
2408
|
+
} else {
|
|
2409
|
+
const verify = cryptoModule.createVerify("RSA-SHA256");
|
|
2410
|
+
verify.update(data);
|
|
2411
|
+
return verify.verify(publicKey, signature, "base64");
|
|
2412
|
+
}
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
this.logger.error("Signature verification failed", error);
|
|
2415
|
+
return false;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
async verifyCryptoSignatureBrowser(_data, _signature, _publicKey) {
|
|
2419
|
+
this.logger.warn("Browser signature verification not yet implemented");
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
validateConfig() {
|
|
2423
|
+
if (!this.config.trustedPublicKeys || this.config.trustedPublicKeys.size === 0) {
|
|
2424
|
+
this.logger.warn("No trusted public keys configured - all signatures will fail");
|
|
2425
|
+
}
|
|
2426
|
+
if (!this.config.algorithm) {
|
|
2427
|
+
throw new Error("Signature algorithm must be specified");
|
|
2428
|
+
}
|
|
2429
|
+
if (!["RS256", "ES256"].includes(this.config.algorithm)) {
|
|
2430
|
+
throw new Error(`Unsupported algorithm: ${this.config.algorithm}`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2434
|
+
|
|
2435
|
+
// src/security/plugin-permission-enforcer.ts
|
|
2436
|
+
var PluginPermissionEnforcer = class {
|
|
2437
|
+
constructor(logger) {
|
|
2438
|
+
this.permissionRegistry = /* @__PURE__ */ new Map();
|
|
2439
|
+
this.capabilityRegistry = /* @__PURE__ */ new Map();
|
|
2440
|
+
this.logger = logger;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Register plugin capabilities and build permission set
|
|
2444
|
+
*
|
|
2445
|
+
* @param pluginName - Plugin identifier
|
|
2446
|
+
* @param capabilities - Array of capability declarations
|
|
2447
|
+
*/
|
|
2448
|
+
registerPluginPermissions(pluginName, capabilities) {
|
|
2449
|
+
this.capabilityRegistry.set(pluginName, capabilities);
|
|
2450
|
+
const permissions = {
|
|
2451
|
+
canAccessService: (service) => this.checkServiceAccess(capabilities, service),
|
|
2452
|
+
canTriggerHook: (hook) => this.checkHookAccess(capabilities, hook),
|
|
2453
|
+
canReadFile: (path) => this.checkFileRead(capabilities, path),
|
|
2454
|
+
canWriteFile: (path) => this.checkFileWrite(capabilities, path),
|
|
2455
|
+
canNetworkRequest: (url) => this.checkNetworkAccess(capabilities, url)
|
|
2456
|
+
};
|
|
2457
|
+
this.permissionRegistry.set(pluginName, permissions);
|
|
2458
|
+
this.logger.info(`Permissions registered for plugin: ${pluginName}`, {
|
|
2459
|
+
plugin: pluginName,
|
|
2460
|
+
capabilityCount: capabilities.length
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Enforce service access permission
|
|
2465
|
+
*
|
|
2466
|
+
* @param pluginName - Plugin requesting access
|
|
2467
|
+
* @param serviceName - Service to access
|
|
2468
|
+
* @throws Error if permission denied
|
|
2469
|
+
*/
|
|
2470
|
+
enforceServiceAccess(pluginName, serviceName) {
|
|
2471
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canAccessService(serviceName));
|
|
2472
|
+
if (!result.allowed) {
|
|
2473
|
+
const error = `Permission denied: Plugin ${pluginName} cannot access service ${serviceName}`;
|
|
2474
|
+
this.logger.warn(error, {
|
|
2475
|
+
plugin: pluginName,
|
|
2476
|
+
service: serviceName,
|
|
2477
|
+
reason: result.reason
|
|
2478
|
+
});
|
|
2479
|
+
throw new Error(error);
|
|
2480
|
+
}
|
|
2481
|
+
this.logger.debug(`Service access granted: ${pluginName} -> ${serviceName}`);
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Enforce hook trigger permission
|
|
2485
|
+
*
|
|
2486
|
+
* @param pluginName - Plugin requesting access
|
|
2487
|
+
* @param hookName - Hook to trigger
|
|
2488
|
+
* @throws Error if permission denied
|
|
2489
|
+
*/
|
|
2490
|
+
enforceHookTrigger(pluginName, hookName) {
|
|
2491
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canTriggerHook(hookName));
|
|
2492
|
+
if (!result.allowed) {
|
|
2493
|
+
const error = `Permission denied: Plugin ${pluginName} cannot trigger hook ${hookName}`;
|
|
2494
|
+
this.logger.warn(error, {
|
|
2495
|
+
plugin: pluginName,
|
|
2496
|
+
hook: hookName,
|
|
2497
|
+
reason: result.reason
|
|
2498
|
+
});
|
|
2499
|
+
throw new Error(error);
|
|
2500
|
+
}
|
|
2501
|
+
this.logger.debug(`Hook trigger granted: ${pluginName} -> ${hookName}`);
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Enforce file read permission
|
|
2505
|
+
*
|
|
2506
|
+
* @param pluginName - Plugin requesting access
|
|
2507
|
+
* @param path - File path to read
|
|
2508
|
+
* @throws Error if permission denied
|
|
2509
|
+
*/
|
|
2510
|
+
enforceFileRead(pluginName, path) {
|
|
2511
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canReadFile(path));
|
|
2512
|
+
if (!result.allowed) {
|
|
2513
|
+
const error = `Permission denied: Plugin ${pluginName} cannot read file ${path}`;
|
|
2514
|
+
this.logger.warn(error, {
|
|
2515
|
+
plugin: pluginName,
|
|
2516
|
+
path,
|
|
2517
|
+
reason: result.reason
|
|
2518
|
+
});
|
|
2519
|
+
throw new Error(error);
|
|
2520
|
+
}
|
|
2521
|
+
this.logger.debug(`File read granted: ${pluginName} -> ${path}`);
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Enforce file write permission
|
|
2525
|
+
*
|
|
2526
|
+
* @param pluginName - Plugin requesting access
|
|
2527
|
+
* @param path - File path to write
|
|
2528
|
+
* @throws Error if permission denied
|
|
2529
|
+
*/
|
|
2530
|
+
enforceFileWrite(pluginName, path) {
|
|
2531
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canWriteFile(path));
|
|
2532
|
+
if (!result.allowed) {
|
|
2533
|
+
const error = `Permission denied: Plugin ${pluginName} cannot write file ${path}`;
|
|
2534
|
+
this.logger.warn(error, {
|
|
2535
|
+
plugin: pluginName,
|
|
2536
|
+
path,
|
|
2537
|
+
reason: result.reason
|
|
2538
|
+
});
|
|
2539
|
+
throw new Error(error);
|
|
2540
|
+
}
|
|
2541
|
+
this.logger.debug(`File write granted: ${pluginName} -> ${path}`);
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Enforce network request permission
|
|
2545
|
+
*
|
|
2546
|
+
* @param pluginName - Plugin requesting access
|
|
2547
|
+
* @param url - URL to access
|
|
2548
|
+
* @throws Error if permission denied
|
|
2549
|
+
*/
|
|
2550
|
+
enforceNetworkRequest(pluginName, url) {
|
|
2551
|
+
const result = this.checkPermission(pluginName, (perms) => perms.canNetworkRequest(url));
|
|
2552
|
+
if (!result.allowed) {
|
|
2553
|
+
const error = `Permission denied: Plugin ${pluginName} cannot access URL ${url}`;
|
|
2554
|
+
this.logger.warn(error, {
|
|
2555
|
+
plugin: pluginName,
|
|
2556
|
+
url,
|
|
2557
|
+
reason: result.reason
|
|
2558
|
+
});
|
|
2559
|
+
throw new Error(error);
|
|
2560
|
+
}
|
|
2561
|
+
this.logger.debug(`Network request granted: ${pluginName} -> ${url}`);
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Get plugin capabilities
|
|
2565
|
+
*
|
|
2566
|
+
* @param pluginName - Plugin identifier
|
|
2567
|
+
* @returns Array of capabilities or undefined
|
|
2568
|
+
*/
|
|
2569
|
+
getPluginCapabilities(pluginName) {
|
|
2570
|
+
return this.capabilityRegistry.get(pluginName);
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Get plugin permissions
|
|
2574
|
+
*
|
|
2575
|
+
* @param pluginName - Plugin identifier
|
|
2576
|
+
* @returns Permissions object or undefined
|
|
2577
|
+
*/
|
|
2578
|
+
getPluginPermissions(pluginName) {
|
|
2579
|
+
return this.permissionRegistry.get(pluginName);
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* Revoke all permissions for a plugin
|
|
2583
|
+
*
|
|
2584
|
+
* @param pluginName - Plugin identifier
|
|
2585
|
+
*/
|
|
2586
|
+
revokePermissions(pluginName) {
|
|
2587
|
+
this.permissionRegistry.delete(pluginName);
|
|
2588
|
+
this.capabilityRegistry.delete(pluginName);
|
|
2589
|
+
this.logger.warn(`Permissions revoked for plugin: ${pluginName}`);
|
|
2590
|
+
}
|
|
2591
|
+
// Private methods
|
|
2592
|
+
checkPermission(pluginName, check) {
|
|
2593
|
+
const permissions = this.permissionRegistry.get(pluginName);
|
|
2594
|
+
if (!permissions) {
|
|
2595
|
+
return {
|
|
2596
|
+
allowed: false,
|
|
2597
|
+
reason: "Plugin permissions not registered"
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
const allowed = check(permissions);
|
|
2601
|
+
return {
|
|
2602
|
+
allowed,
|
|
2603
|
+
reason: allowed ? void 0 : "No matching capability found"
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
checkServiceAccess(capabilities, serviceName) {
|
|
2607
|
+
return capabilities.some((cap) => {
|
|
2608
|
+
const protocolId = cap.protocol.id;
|
|
2609
|
+
if (protocolId.includes("protocol.service.all")) {
|
|
2610
|
+
return true;
|
|
2611
|
+
}
|
|
2612
|
+
if (protocolId.includes(`protocol.service.${serviceName}`)) {
|
|
2613
|
+
return true;
|
|
2614
|
+
}
|
|
2615
|
+
const serviceCategory = serviceName.split(".")[0];
|
|
2616
|
+
if (protocolId.includes(`protocol.service.${serviceCategory}`)) {
|
|
2617
|
+
return true;
|
|
2618
|
+
}
|
|
2619
|
+
return false;
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
checkHookAccess(capabilities, hookName) {
|
|
2623
|
+
return capabilities.some((cap) => {
|
|
2624
|
+
const protocolId = cap.protocol.id;
|
|
2625
|
+
if (protocolId.includes("protocol.hook.all")) {
|
|
2626
|
+
return true;
|
|
2627
|
+
}
|
|
2628
|
+
if (protocolId.includes(`protocol.hook.${hookName}`)) {
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
const hookCategory = hookName.split(":")[0];
|
|
2632
|
+
if (protocolId.includes(`protocol.hook.${hookCategory}`)) {
|
|
2633
|
+
return true;
|
|
2634
|
+
}
|
|
2635
|
+
return false;
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
checkFileRead(capabilities, _path) {
|
|
2639
|
+
return capabilities.some((cap) => {
|
|
2640
|
+
const protocolId = cap.protocol.id;
|
|
2641
|
+
if (protocolId.includes("protocol.filesystem.read")) {
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
return false;
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
checkFileWrite(capabilities, _path) {
|
|
2648
|
+
return capabilities.some((cap) => {
|
|
2649
|
+
const protocolId = cap.protocol.id;
|
|
2650
|
+
if (protocolId.includes("protocol.filesystem.write")) {
|
|
2651
|
+
return true;
|
|
2652
|
+
}
|
|
2653
|
+
return false;
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
checkNetworkAccess(capabilities, _url) {
|
|
2657
|
+
return capabilities.some((cap) => {
|
|
2658
|
+
const protocolId = cap.protocol.id;
|
|
2659
|
+
if (protocolId.includes("protocol.network")) {
|
|
2660
|
+
return true;
|
|
2661
|
+
}
|
|
2662
|
+
return false;
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
var SecurePluginContext = class {
|
|
2667
|
+
constructor(pluginName, permissionEnforcer, baseContext) {
|
|
2668
|
+
this.pluginName = pluginName;
|
|
2669
|
+
this.permissionEnforcer = permissionEnforcer;
|
|
2670
|
+
this.baseContext = baseContext;
|
|
2671
|
+
}
|
|
2672
|
+
registerService(name, service) {
|
|
2673
|
+
this.baseContext.registerService(name, service);
|
|
2674
|
+
}
|
|
2675
|
+
getService(name) {
|
|
2676
|
+
this.permissionEnforcer.enforceServiceAccess(this.pluginName, name);
|
|
2677
|
+
return this.baseContext.getService(name);
|
|
2678
|
+
}
|
|
2679
|
+
getServices() {
|
|
2680
|
+
return this.baseContext.getServices();
|
|
2681
|
+
}
|
|
2682
|
+
hook(name, handler) {
|
|
2683
|
+
this.baseContext.hook(name, handler);
|
|
2684
|
+
}
|
|
2685
|
+
async trigger(name, ...args) {
|
|
2686
|
+
this.permissionEnforcer.enforceHookTrigger(this.pluginName, name);
|
|
2687
|
+
await this.baseContext.trigger(name, ...args);
|
|
2688
|
+
}
|
|
2689
|
+
get logger() {
|
|
2690
|
+
return this.baseContext.logger;
|
|
2691
|
+
}
|
|
2692
|
+
getKernel() {
|
|
2693
|
+
return this.baseContext.getKernel();
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
function createPluginPermissionEnforcer(logger) {
|
|
2697
|
+
return new PluginPermissionEnforcer(logger);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
// src/security/permission-manager.ts
|
|
2701
|
+
var PluginPermissionManager = class {
|
|
2702
|
+
constructor(logger) {
|
|
2703
|
+
// Plugin permission definitions
|
|
2704
|
+
this.permissionSets = /* @__PURE__ */ new Map();
|
|
2705
|
+
// Granted permissions (pluginId -> Set of permission IDs)
|
|
2706
|
+
this.grants = /* @__PURE__ */ new Map();
|
|
2707
|
+
// Permission grant details
|
|
2708
|
+
this.grantDetails = /* @__PURE__ */ new Map();
|
|
2709
|
+
this.logger = logger.child({ component: "PermissionManager" });
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Register permission requirements for a plugin
|
|
2713
|
+
*/
|
|
2714
|
+
registerPermissions(pluginId, permissionSet) {
|
|
2715
|
+
this.permissionSets.set(pluginId, permissionSet);
|
|
2716
|
+
this.logger.info("Permissions registered for plugin", {
|
|
2717
|
+
pluginId,
|
|
2718
|
+
permissionCount: permissionSet.permissions.length
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Grant a permission to a plugin
|
|
2723
|
+
*/
|
|
2724
|
+
grantPermission(pluginId, permissionId, grantedBy, expiresAt) {
|
|
2725
|
+
const permissionSet = this.permissionSets.get(pluginId);
|
|
2726
|
+
if (!permissionSet) {
|
|
2727
|
+
throw new Error(`No permissions registered for plugin: ${pluginId}`);
|
|
2728
|
+
}
|
|
2729
|
+
const permission = permissionSet.permissions.find((p) => p.id === permissionId);
|
|
2730
|
+
if (!permission) {
|
|
2731
|
+
throw new Error(`Permission ${permissionId} not declared by plugin ${pluginId}`);
|
|
2732
|
+
}
|
|
2733
|
+
if (!this.grants.has(pluginId)) {
|
|
2734
|
+
this.grants.set(pluginId, /* @__PURE__ */ new Set());
|
|
2735
|
+
}
|
|
2736
|
+
this.grants.get(pluginId).add(permissionId);
|
|
2737
|
+
const grantKey = `${pluginId}:${permissionId}`;
|
|
2738
|
+
this.grantDetails.set(grantKey, {
|
|
2739
|
+
permissionId,
|
|
2740
|
+
pluginId,
|
|
2741
|
+
grantedAt: /* @__PURE__ */ new Date(),
|
|
2742
|
+
grantedBy,
|
|
2743
|
+
expiresAt
|
|
2744
|
+
});
|
|
2745
|
+
this.logger.info("Permission granted", {
|
|
2746
|
+
pluginId,
|
|
2747
|
+
permissionId,
|
|
2748
|
+
grantedBy
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Revoke a permission from a plugin
|
|
2753
|
+
*/
|
|
2754
|
+
revokePermission(pluginId, permissionId) {
|
|
2755
|
+
const grants = this.grants.get(pluginId);
|
|
2756
|
+
if (grants) {
|
|
2757
|
+
grants.delete(permissionId);
|
|
2758
|
+
const grantKey = `${pluginId}:${permissionId}`;
|
|
2759
|
+
this.grantDetails.delete(grantKey);
|
|
2760
|
+
this.logger.info("Permission revoked", { pluginId, permissionId });
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Grant all permissions for a plugin
|
|
2765
|
+
*/
|
|
2766
|
+
grantAllPermissions(pluginId, grantedBy) {
|
|
2767
|
+
const permissionSet = this.permissionSets.get(pluginId);
|
|
2768
|
+
if (!permissionSet) {
|
|
2769
|
+
throw new Error(`No permissions registered for plugin: ${pluginId}`);
|
|
2770
|
+
}
|
|
2771
|
+
for (const permission of permissionSet.permissions) {
|
|
2772
|
+
this.grantPermission(pluginId, permission.id, grantedBy);
|
|
2773
|
+
}
|
|
2774
|
+
this.logger.info("All permissions granted", { pluginId, grantedBy });
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Check if a plugin has a specific permission
|
|
2778
|
+
*/
|
|
2779
|
+
hasPermission(pluginId, permissionId) {
|
|
2780
|
+
const grants = this.grants.get(pluginId);
|
|
2781
|
+
if (!grants) {
|
|
2782
|
+
return false;
|
|
2783
|
+
}
|
|
2784
|
+
if (!grants.has(permissionId)) {
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
const grantKey = `${pluginId}:${permissionId}`;
|
|
2788
|
+
const grantDetails = this.grantDetails.get(grantKey);
|
|
2789
|
+
if (grantDetails?.expiresAt && grantDetails.expiresAt < /* @__PURE__ */ new Date()) {
|
|
2790
|
+
this.revokePermission(pluginId, permissionId);
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
return true;
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Check if plugin can perform an action on a resource
|
|
2797
|
+
*/
|
|
2798
|
+
checkAccess(pluginId, resource, action, resourceId) {
|
|
2799
|
+
const permissionSet = this.permissionSets.get(pluginId);
|
|
2800
|
+
if (!permissionSet) {
|
|
2801
|
+
return {
|
|
2802
|
+
allowed: false,
|
|
2803
|
+
reason: "No permissions registered for plugin"
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
const matchingPermissions = permissionSet.permissions.filter((p) => {
|
|
2807
|
+
if (p.resource !== resource) {
|
|
2808
|
+
return false;
|
|
2809
|
+
}
|
|
2810
|
+
if (!p.actions.includes(action)) {
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
if (resourceId && p.filter?.resourceIds) {
|
|
2814
|
+
if (!p.filter.resourceIds.includes(resourceId)) {
|
|
2815
|
+
return false;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return true;
|
|
2819
|
+
});
|
|
2820
|
+
if (matchingPermissions.length === 0) {
|
|
2821
|
+
return {
|
|
2822
|
+
allowed: false,
|
|
2823
|
+
reason: `No permission found for ${action} on ${resource}`
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
const grantedPermissions = matchingPermissions.filter(
|
|
2827
|
+
(p) => this.hasPermission(pluginId, p.id)
|
|
2828
|
+
);
|
|
2829
|
+
if (grantedPermissions.length === 0) {
|
|
2830
|
+
return {
|
|
2831
|
+
allowed: false,
|
|
2832
|
+
reason: "Required permissions not granted",
|
|
2833
|
+
requiredPermission: matchingPermissions[0].id
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
allowed: true,
|
|
2838
|
+
grantedPermissions: grantedPermissions.map((p) => p.id)
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
/**
|
|
2842
|
+
* Get all permissions for a plugin
|
|
2843
|
+
*/
|
|
2844
|
+
getPluginPermissions(pluginId) {
|
|
2845
|
+
const permissionSet = this.permissionSets.get(pluginId);
|
|
2846
|
+
return permissionSet?.permissions || [];
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Get granted permissions for a plugin
|
|
2850
|
+
*/
|
|
2851
|
+
getGrantedPermissions(pluginId) {
|
|
2852
|
+
const grants = this.grants.get(pluginId);
|
|
2853
|
+
return grants ? Array.from(grants) : [];
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Get required but not granted permissions
|
|
2857
|
+
*/
|
|
2858
|
+
getMissingPermissions(pluginId) {
|
|
2859
|
+
const permissionSet = this.permissionSets.get(pluginId);
|
|
2860
|
+
if (!permissionSet) {
|
|
2861
|
+
return [];
|
|
2862
|
+
}
|
|
2863
|
+
const granted = this.grants.get(pluginId) || /* @__PURE__ */ new Set();
|
|
2864
|
+
return permissionSet.permissions.filter(
|
|
2865
|
+
(p) => p.required && !granted.has(p.id)
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Check if all required permissions are granted
|
|
2870
|
+
*/
|
|
2871
|
+
hasAllRequiredPermissions(pluginId) {
|
|
2872
|
+
return this.getMissingPermissions(pluginId).length === 0;
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* Get permission grant details
|
|
2876
|
+
*/
|
|
2877
|
+
getGrantDetails(pluginId, permissionId) {
|
|
2878
|
+
const grantKey = `${pluginId}:${permissionId}`;
|
|
2879
|
+
return this.grantDetails.get(grantKey);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Validate permission against scope constraints
|
|
2883
|
+
*/
|
|
2884
|
+
validatePermissionScope(permission, context) {
|
|
2885
|
+
switch (permission.scope) {
|
|
2886
|
+
case "global":
|
|
2887
|
+
return true;
|
|
2888
|
+
case "tenant":
|
|
2889
|
+
return !!context.tenantId;
|
|
2890
|
+
case "user":
|
|
2891
|
+
return !!context.userId;
|
|
2892
|
+
case "resource":
|
|
2893
|
+
return !!context.resourceId;
|
|
2894
|
+
case "plugin":
|
|
2895
|
+
return true;
|
|
2896
|
+
default:
|
|
2897
|
+
return false;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Clear all permissions for a plugin
|
|
2902
|
+
*/
|
|
2903
|
+
clearPluginPermissions(pluginId) {
|
|
2904
|
+
this.permissionSets.delete(pluginId);
|
|
2905
|
+
const grants = this.grants.get(pluginId);
|
|
2906
|
+
if (grants) {
|
|
2907
|
+
for (const permissionId of grants) {
|
|
2908
|
+
const grantKey = `${pluginId}:${permissionId}`;
|
|
2909
|
+
this.grantDetails.delete(grantKey);
|
|
2910
|
+
}
|
|
2911
|
+
this.grants.delete(pluginId);
|
|
2912
|
+
}
|
|
2913
|
+
this.logger.info("All permissions cleared", { pluginId });
|
|
2914
|
+
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Shutdown permission manager
|
|
2917
|
+
*/
|
|
2918
|
+
shutdown() {
|
|
2919
|
+
this.permissionSets.clear();
|
|
2920
|
+
this.grants.clear();
|
|
2921
|
+
this.grantDetails.clear();
|
|
2922
|
+
this.logger.info("Permission manager shutdown complete");
|
|
2923
|
+
}
|
|
2924
|
+
};
|
|
2925
|
+
|
|
2926
|
+
// src/security/sandbox-runtime.ts
|
|
2927
|
+
var PluginSandboxRuntime = class {
|
|
2928
|
+
constructor(logger) {
|
|
2929
|
+
// Active sandboxes (pluginId -> context)
|
|
2930
|
+
this.sandboxes = /* @__PURE__ */ new Map();
|
|
2931
|
+
// Resource monitoring intervals
|
|
2932
|
+
this.monitoringIntervals = /* @__PURE__ */ new Map();
|
|
2933
|
+
this.logger = logger.child({ component: "SandboxRuntime" });
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Create a sandbox for a plugin
|
|
2937
|
+
*/
|
|
2938
|
+
createSandbox(pluginId, config) {
|
|
2939
|
+
if (this.sandboxes.has(pluginId)) {
|
|
2940
|
+
throw new Error(`Sandbox already exists for plugin: ${pluginId}`);
|
|
2941
|
+
}
|
|
2942
|
+
const context = {
|
|
2943
|
+
pluginId,
|
|
2944
|
+
config,
|
|
2945
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
2946
|
+
resourceUsage: {
|
|
2947
|
+
memory: { current: 0, peak: 0, limit: config.memory?.maxHeap },
|
|
2948
|
+
cpu: { current: 0, average: 0, limit: config.cpu?.maxCpuPercent },
|
|
2949
|
+
connections: { current: 0, limit: config.network?.maxConnections }
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
this.sandboxes.set(pluginId, context);
|
|
2953
|
+
this.startResourceMonitoring(pluginId);
|
|
2954
|
+
this.logger.info("Sandbox created", {
|
|
2955
|
+
pluginId,
|
|
2956
|
+
level: config.level,
|
|
2957
|
+
memoryLimit: config.memory?.maxHeap,
|
|
2958
|
+
cpuLimit: config.cpu?.maxCpuPercent
|
|
2959
|
+
});
|
|
2960
|
+
return context;
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Destroy a sandbox
|
|
2964
|
+
*/
|
|
2965
|
+
destroySandbox(pluginId) {
|
|
2966
|
+
const context = this.sandboxes.get(pluginId);
|
|
2967
|
+
if (!context) {
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
this.stopResourceMonitoring(pluginId);
|
|
2971
|
+
this.sandboxes.delete(pluginId);
|
|
2972
|
+
this.logger.info("Sandbox destroyed", { pluginId });
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Check if resource access is allowed
|
|
2976
|
+
*/
|
|
2977
|
+
checkResourceAccess(pluginId, resourceType, resourcePath) {
|
|
2978
|
+
const context = this.sandboxes.get(pluginId);
|
|
2979
|
+
if (!context) {
|
|
2980
|
+
return { allowed: false, reason: "Sandbox not found" };
|
|
2981
|
+
}
|
|
2982
|
+
const { config } = context;
|
|
2983
|
+
switch (resourceType) {
|
|
2984
|
+
case "file":
|
|
2985
|
+
return this.checkFileAccess(config, resourcePath);
|
|
2986
|
+
case "network":
|
|
2987
|
+
return this.checkNetworkAccess(config, resourcePath);
|
|
2988
|
+
case "process":
|
|
2989
|
+
return this.checkProcessAccess(config);
|
|
2990
|
+
case "env":
|
|
2991
|
+
return this.checkEnvAccess(config, resourcePath);
|
|
2992
|
+
default:
|
|
2993
|
+
return { allowed: false, reason: "Unknown resource type" };
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Check file system access
|
|
2998
|
+
* WARNING: Uses simple prefix matching. For production, use proper path
|
|
2999
|
+
* resolution with path.resolve() and path.normalize() to prevent traversal.
|
|
3000
|
+
*/
|
|
3001
|
+
checkFileAccess(config, path) {
|
|
3002
|
+
if (config.level === "none") {
|
|
3003
|
+
return { allowed: true };
|
|
3004
|
+
}
|
|
3005
|
+
if (!config.filesystem) {
|
|
3006
|
+
return { allowed: false, reason: "File system access not configured" };
|
|
3007
|
+
}
|
|
3008
|
+
if (!path) {
|
|
3009
|
+
return { allowed: config.filesystem.mode !== "none" };
|
|
3010
|
+
}
|
|
3011
|
+
const allowedPaths = config.filesystem.allowedPaths || [];
|
|
3012
|
+
const isAllowed = allowedPaths.some((allowed) => {
|
|
3013
|
+
return path.startsWith(allowed);
|
|
3014
|
+
});
|
|
3015
|
+
if (allowedPaths.length > 0 && !isAllowed) {
|
|
3016
|
+
return {
|
|
3017
|
+
allowed: false,
|
|
3018
|
+
reason: `Path not in allowed list: ${path}`
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
const deniedPaths = config.filesystem.deniedPaths || [];
|
|
3022
|
+
const isDenied = deniedPaths.some((denied) => {
|
|
3023
|
+
return path.startsWith(denied);
|
|
3024
|
+
});
|
|
3025
|
+
if (isDenied) {
|
|
3026
|
+
return {
|
|
3027
|
+
allowed: false,
|
|
3028
|
+
reason: `Path is explicitly denied: ${path}`
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
return { allowed: true };
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Check network access
|
|
3035
|
+
* WARNING: Uses simple string matching. For production, use proper URL
|
|
3036
|
+
* parsing with new URL() and check hostname property.
|
|
3037
|
+
*/
|
|
3038
|
+
checkNetworkAccess(config, url) {
|
|
3039
|
+
if (config.level === "none") {
|
|
3040
|
+
return { allowed: true };
|
|
3041
|
+
}
|
|
3042
|
+
if (!config.network) {
|
|
3043
|
+
return { allowed: false, reason: "Network access not configured" };
|
|
3044
|
+
}
|
|
3045
|
+
if (config.network.mode === "none") {
|
|
3046
|
+
return { allowed: false, reason: "Network access disabled" };
|
|
3047
|
+
}
|
|
3048
|
+
if (!url) {
|
|
3049
|
+
return { allowed: config.network.mode !== "none" };
|
|
3050
|
+
}
|
|
3051
|
+
const allowedHosts = config.network.allowedHosts || [];
|
|
3052
|
+
if (allowedHosts.length > 0) {
|
|
3053
|
+
const isAllowed = allowedHosts.some((host) => {
|
|
3054
|
+
return url.includes(host);
|
|
3055
|
+
});
|
|
3056
|
+
if (!isAllowed) {
|
|
3057
|
+
return {
|
|
3058
|
+
allowed: false,
|
|
3059
|
+
reason: `Host not in allowed list: ${url}`
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
const deniedHosts = config.network.deniedHosts || [];
|
|
3064
|
+
const isDenied = deniedHosts.some((host) => {
|
|
3065
|
+
return url.includes(host);
|
|
3066
|
+
});
|
|
3067
|
+
if (isDenied) {
|
|
3068
|
+
return {
|
|
3069
|
+
allowed: false,
|
|
3070
|
+
reason: `Host is blocked: ${url}`
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
return { allowed: true };
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Check process spawning access
|
|
3077
|
+
*/
|
|
3078
|
+
checkProcessAccess(config) {
|
|
3079
|
+
if (config.level === "none") {
|
|
3080
|
+
return { allowed: true };
|
|
3081
|
+
}
|
|
3082
|
+
if (!config.process) {
|
|
3083
|
+
return { allowed: false, reason: "Process access not configured" };
|
|
3084
|
+
}
|
|
3085
|
+
if (!config.process.allowSpawn) {
|
|
3086
|
+
return { allowed: false, reason: "Process spawning not allowed" };
|
|
3087
|
+
}
|
|
3088
|
+
return { allowed: true };
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Check environment variable access
|
|
3092
|
+
*/
|
|
3093
|
+
checkEnvAccess(config, varName) {
|
|
3094
|
+
if (config.level === "none") {
|
|
3095
|
+
return { allowed: true };
|
|
3096
|
+
}
|
|
3097
|
+
if (!config.process) {
|
|
3098
|
+
return { allowed: false, reason: "Environment access not configured" };
|
|
3099
|
+
}
|
|
3100
|
+
if (!varName) {
|
|
3101
|
+
return { allowed: true };
|
|
3102
|
+
}
|
|
3103
|
+
return { allowed: true };
|
|
3104
|
+
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Check resource limits
|
|
3107
|
+
*/
|
|
3108
|
+
checkResourceLimits(pluginId) {
|
|
3109
|
+
const context = this.sandboxes.get(pluginId);
|
|
3110
|
+
if (!context) {
|
|
3111
|
+
return { withinLimits: true, violations: [] };
|
|
3112
|
+
}
|
|
3113
|
+
const violations = [];
|
|
3114
|
+
const { resourceUsage, config } = context;
|
|
3115
|
+
if (config.memory?.maxHeap && resourceUsage.memory.current > config.memory.maxHeap) {
|
|
3116
|
+
violations.push(`Memory limit exceeded: ${resourceUsage.memory.current} > ${config.memory.maxHeap}`);
|
|
3117
|
+
}
|
|
3118
|
+
if (config.runtime?.resourceLimits?.maxCpu && resourceUsage.cpu.current > config.runtime.resourceLimits.maxCpu) {
|
|
3119
|
+
violations.push(`CPU limit exceeded: ${resourceUsage.cpu.current}% > ${config.runtime.resourceLimits.maxCpu}%`);
|
|
3120
|
+
}
|
|
3121
|
+
if (config.network?.maxConnections && resourceUsage.connections.current > config.network.maxConnections) {
|
|
3122
|
+
violations.push(`Connection limit exceeded: ${resourceUsage.connections.current} > ${config.network.maxConnections}`);
|
|
3123
|
+
}
|
|
3124
|
+
return {
|
|
3125
|
+
withinLimits: violations.length === 0,
|
|
3126
|
+
violations
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Get resource usage for a plugin
|
|
3131
|
+
*/
|
|
3132
|
+
getResourceUsage(pluginId) {
|
|
3133
|
+
const context = this.sandboxes.get(pluginId);
|
|
3134
|
+
return context?.resourceUsage;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Start monitoring resource usage
|
|
3138
|
+
*/
|
|
3139
|
+
startResourceMonitoring(pluginId) {
|
|
3140
|
+
const interval = setInterval(() => {
|
|
3141
|
+
this.updateResourceUsage(pluginId);
|
|
3142
|
+
}, 5e3);
|
|
3143
|
+
this.monitoringIntervals.set(pluginId, interval);
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Stop monitoring resource usage
|
|
3147
|
+
*/
|
|
3148
|
+
stopResourceMonitoring(pluginId) {
|
|
3149
|
+
const interval = this.monitoringIntervals.get(pluginId);
|
|
3150
|
+
if (interval) {
|
|
3151
|
+
clearInterval(interval);
|
|
3152
|
+
this.monitoringIntervals.delete(pluginId);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Update resource usage statistics
|
|
3157
|
+
*
|
|
3158
|
+
* NOTE: Currently uses global process.memoryUsage() which tracks the entire
|
|
3159
|
+
* Node.js process, not individual plugins. For production, implement proper
|
|
3160
|
+
* per-plugin tracking using V8 heap snapshots or allocation tracking at
|
|
3161
|
+
* plugin boundaries.
|
|
3162
|
+
*/
|
|
3163
|
+
updateResourceUsage(pluginId) {
|
|
3164
|
+
const context = this.sandboxes.get(pluginId);
|
|
3165
|
+
if (!context) {
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
const memoryUsage = getMemoryUsage();
|
|
3169
|
+
context.resourceUsage.memory.current = memoryUsage.heapUsed;
|
|
3170
|
+
context.resourceUsage.memory.peak = Math.max(
|
|
3171
|
+
context.resourceUsage.memory.peak,
|
|
3172
|
+
memoryUsage.heapUsed
|
|
3173
|
+
);
|
|
3174
|
+
context.resourceUsage.cpu.current = 0;
|
|
3175
|
+
const { withinLimits, violations } = this.checkResourceLimits(pluginId);
|
|
3176
|
+
if (!withinLimits) {
|
|
3177
|
+
this.logger.warn("Resource limit violations detected", {
|
|
3178
|
+
pluginId,
|
|
3179
|
+
violations
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Get all active sandboxes
|
|
3185
|
+
*/
|
|
3186
|
+
getAllSandboxes() {
|
|
3187
|
+
return new Map(this.sandboxes);
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Shutdown sandbox runtime
|
|
3191
|
+
*/
|
|
3192
|
+
shutdown() {
|
|
3193
|
+
for (const pluginId of this.monitoringIntervals.keys()) {
|
|
3194
|
+
this.stopResourceMonitoring(pluginId);
|
|
3195
|
+
}
|
|
3196
|
+
this.sandboxes.clear();
|
|
3197
|
+
this.logger.info("Sandbox runtime shutdown complete");
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
|
|
3201
|
+
// src/security/security-scanner.ts
|
|
3202
|
+
var PluginSecurityScanner = class {
|
|
3203
|
+
constructor(logger, config) {
|
|
3204
|
+
// Known vulnerabilities database (CVE cache)
|
|
3205
|
+
this.vulnerabilityDb = /* @__PURE__ */ new Map();
|
|
3206
|
+
// Scan results cache
|
|
3207
|
+
this.scanResults = /* @__PURE__ */ new Map();
|
|
3208
|
+
this.passThreshold = 70;
|
|
3209
|
+
this.logger = logger.child({ component: "SecurityScanner" });
|
|
3210
|
+
if (config?.passThreshold !== void 0) {
|
|
3211
|
+
this.passThreshold = config.passThreshold;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Perform a comprehensive security scan on a plugin
|
|
3216
|
+
*/
|
|
3217
|
+
async scan(target) {
|
|
3218
|
+
this.logger.info("Starting security scan", {
|
|
3219
|
+
pluginId: target.pluginId,
|
|
3220
|
+
version: target.version
|
|
3221
|
+
});
|
|
3222
|
+
const issues = [];
|
|
3223
|
+
try {
|
|
3224
|
+
const codeIssues = await this.scanCode(target);
|
|
3225
|
+
issues.push(...codeIssues);
|
|
3226
|
+
const depIssues = await this.scanDependencies(target);
|
|
3227
|
+
issues.push(...depIssues);
|
|
3228
|
+
const malwareIssues = await this.scanMalware(target);
|
|
3229
|
+
issues.push(...malwareIssues);
|
|
3230
|
+
const licenseIssues = await this.scanLicenses(target);
|
|
3231
|
+
issues.push(...licenseIssues);
|
|
3232
|
+
const configIssues = await this.scanConfiguration(target);
|
|
3233
|
+
issues.push(...configIssues);
|
|
3234
|
+
const score = this.calculateSecurityScore(issues);
|
|
3235
|
+
const result = {
|
|
3236
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3237
|
+
scanner: { name: "ObjectStack Security Scanner", version: "1.0.0" },
|
|
3238
|
+
status: score >= this.passThreshold ? "passed" : "failed",
|
|
3239
|
+
vulnerabilities: issues.map((issue) => ({
|
|
3240
|
+
id: issue.id,
|
|
3241
|
+
severity: issue.severity,
|
|
3242
|
+
category: issue.category,
|
|
3243
|
+
title: issue.title,
|
|
3244
|
+
description: issue.description,
|
|
3245
|
+
location: issue.location ? `${issue.location.file}:${issue.location.line}` : void 0,
|
|
3246
|
+
remediation: issue.remediation,
|
|
3247
|
+
affectedVersions: [],
|
|
3248
|
+
exploitAvailable: false,
|
|
3249
|
+
patchAvailable: false
|
|
3250
|
+
})),
|
|
3251
|
+
summary: {
|
|
3252
|
+
totalVulnerabilities: issues.length,
|
|
3253
|
+
criticalCount: issues.filter((i) => i.severity === "critical").length,
|
|
3254
|
+
highCount: issues.filter((i) => i.severity === "high").length,
|
|
3255
|
+
mediumCount: issues.filter((i) => i.severity === "medium").length,
|
|
3256
|
+
lowCount: issues.filter((i) => i.severity === "low").length,
|
|
3257
|
+
infoCount: issues.filter((i) => i.severity === "info").length
|
|
3258
|
+
}
|
|
3259
|
+
};
|
|
3260
|
+
this.scanResults.set(`${target.pluginId}:${target.version}`, result);
|
|
3261
|
+
this.logger.info("Security scan complete", {
|
|
3262
|
+
pluginId: target.pluginId,
|
|
3263
|
+
score,
|
|
3264
|
+
status: result.status,
|
|
3265
|
+
summary: result.summary
|
|
3266
|
+
});
|
|
3267
|
+
return result;
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
this.logger.error("Security scan failed", {
|
|
3270
|
+
pluginId: target.pluginId,
|
|
3271
|
+
error
|
|
3272
|
+
});
|
|
3273
|
+
throw error;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Scan code for vulnerabilities
|
|
3278
|
+
*/
|
|
3279
|
+
async scanCode(target) {
|
|
3280
|
+
const issues = [];
|
|
3281
|
+
this.logger.debug("Code scan complete", {
|
|
3282
|
+
pluginId: target.pluginId,
|
|
3283
|
+
issuesFound: issues.length
|
|
3284
|
+
});
|
|
3285
|
+
return issues;
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Scan dependencies for known vulnerabilities
|
|
3289
|
+
*/
|
|
3290
|
+
async scanDependencies(target) {
|
|
3291
|
+
const issues = [];
|
|
3292
|
+
if (!target.dependencies) {
|
|
3293
|
+
return issues;
|
|
3294
|
+
}
|
|
3295
|
+
for (const [depName, version] of Object.entries(target.dependencies)) {
|
|
3296
|
+
const vulnKey = `${depName}@${version}`;
|
|
3297
|
+
const vulnerability = this.vulnerabilityDb.get(vulnKey);
|
|
3298
|
+
if (vulnerability) {
|
|
3299
|
+
issues.push({
|
|
3300
|
+
id: `vuln-${vulnerability.cve || depName}`,
|
|
3301
|
+
severity: vulnerability.severity,
|
|
3302
|
+
category: "vulnerability",
|
|
3303
|
+
title: `Vulnerable dependency: ${depName}`,
|
|
3304
|
+
description: `${depName}@${version} has known security vulnerabilities`,
|
|
3305
|
+
remediation: vulnerability.fixedIn ? `Upgrade to ${vulnerability.fixedIn.join(" or ")}` : "No fix available",
|
|
3306
|
+
cve: vulnerability.cve
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
this.logger.debug("Dependency scan complete", {
|
|
3311
|
+
pluginId: target.pluginId,
|
|
3312
|
+
dependencies: Object.keys(target.dependencies).length,
|
|
3313
|
+
vulnerabilities: issues.length
|
|
3314
|
+
});
|
|
3315
|
+
return issues;
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Scan for malware patterns
|
|
3319
|
+
*/
|
|
3320
|
+
async scanMalware(target) {
|
|
3321
|
+
const issues = [];
|
|
3322
|
+
this.logger.debug("Malware scan complete", {
|
|
3323
|
+
pluginId: target.pluginId,
|
|
3324
|
+
issuesFound: issues.length
|
|
3325
|
+
});
|
|
3326
|
+
return issues;
|
|
3327
|
+
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Check license compliance
|
|
3330
|
+
*/
|
|
3331
|
+
async scanLicenses(target) {
|
|
3332
|
+
const issues = [];
|
|
3333
|
+
if (!target.dependencies) {
|
|
3334
|
+
return issues;
|
|
3335
|
+
}
|
|
3336
|
+
this.logger.debug("License scan complete", {
|
|
3337
|
+
pluginId: target.pluginId,
|
|
3338
|
+
issuesFound: issues.length
|
|
3339
|
+
});
|
|
3340
|
+
return issues;
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Check configuration security
|
|
3344
|
+
*/
|
|
3345
|
+
async scanConfiguration(target) {
|
|
3346
|
+
const issues = [];
|
|
3347
|
+
this.logger.debug("Configuration scan complete", {
|
|
3348
|
+
pluginId: target.pluginId,
|
|
3349
|
+
issuesFound: issues.length
|
|
3350
|
+
});
|
|
3351
|
+
return issues;
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Calculate security score based on issues
|
|
3355
|
+
*/
|
|
3356
|
+
calculateSecurityScore(issues) {
|
|
3357
|
+
let score = 100;
|
|
3358
|
+
for (const issue of issues) {
|
|
3359
|
+
switch (issue.severity) {
|
|
3360
|
+
case "critical":
|
|
3361
|
+
score -= 20;
|
|
3362
|
+
break;
|
|
3363
|
+
case "high":
|
|
3364
|
+
score -= 10;
|
|
3365
|
+
break;
|
|
3366
|
+
case "medium":
|
|
3367
|
+
score -= 5;
|
|
3368
|
+
break;
|
|
3369
|
+
case "low":
|
|
3370
|
+
score -= 2;
|
|
3371
|
+
break;
|
|
3372
|
+
case "info":
|
|
3373
|
+
score -= 0;
|
|
3374
|
+
break;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return Math.max(0, score);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Add a vulnerability to the database
|
|
3381
|
+
*/
|
|
3382
|
+
addVulnerability(packageName, version, vulnerability) {
|
|
3383
|
+
const key = `${packageName}@${version}`;
|
|
3384
|
+
this.vulnerabilityDb.set(key, vulnerability);
|
|
3385
|
+
this.logger.debug("Vulnerability added to database", {
|
|
3386
|
+
package: packageName,
|
|
3387
|
+
version,
|
|
3388
|
+
cve: vulnerability.cve
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* Get scan result from cache
|
|
3393
|
+
*/
|
|
3394
|
+
getScanResult(pluginId, version) {
|
|
3395
|
+
return this.scanResults.get(`${pluginId}:${version}`);
|
|
3396
|
+
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Clear scan results cache
|
|
3399
|
+
*/
|
|
3400
|
+
clearCache() {
|
|
3401
|
+
this.scanResults.clear();
|
|
3402
|
+
this.logger.debug("Scan results cache cleared");
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Update vulnerability database from external source
|
|
3406
|
+
*/
|
|
3407
|
+
async updateVulnerabilityDatabase() {
|
|
3408
|
+
this.logger.info("Updating vulnerability database");
|
|
3409
|
+
this.logger.info("Vulnerability database updated", {
|
|
3410
|
+
entries: this.vulnerabilityDb.size
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Shutdown security scanner
|
|
3415
|
+
*/
|
|
3416
|
+
shutdown() {
|
|
3417
|
+
this.vulnerabilityDb.clear();
|
|
3418
|
+
this.scanResults.clear();
|
|
3419
|
+
this.logger.info("Security scanner shutdown complete");
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
3422
|
+
|
|
3423
|
+
// src/health-monitor.ts
|
|
3424
|
+
var PluginHealthMonitor = class {
|
|
3425
|
+
constructor(logger) {
|
|
3426
|
+
this.healthChecks = /* @__PURE__ */ new Map();
|
|
3427
|
+
this.healthStatus = /* @__PURE__ */ new Map();
|
|
3428
|
+
this.healthReports = /* @__PURE__ */ new Map();
|
|
3429
|
+
this.checkIntervals = /* @__PURE__ */ new Map();
|
|
3430
|
+
this.failureCounters = /* @__PURE__ */ new Map();
|
|
3431
|
+
this.successCounters = /* @__PURE__ */ new Map();
|
|
3432
|
+
this.restartAttempts = /* @__PURE__ */ new Map();
|
|
3433
|
+
this.logger = logger.child({ component: "HealthMonitor" });
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Register a plugin for health monitoring
|
|
3437
|
+
*/
|
|
3438
|
+
registerPlugin(pluginName, config) {
|
|
3439
|
+
this.healthChecks.set(pluginName, config);
|
|
3440
|
+
this.healthStatus.set(pluginName, "unknown");
|
|
3441
|
+
this.failureCounters.set(pluginName, 0);
|
|
3442
|
+
this.successCounters.set(pluginName, 0);
|
|
3443
|
+
this.restartAttempts.set(pluginName, 0);
|
|
3444
|
+
this.logger.info("Plugin registered for health monitoring", {
|
|
3445
|
+
plugin: pluginName,
|
|
3446
|
+
interval: config.interval
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
/**
|
|
3450
|
+
* Start monitoring a plugin
|
|
3451
|
+
*/
|
|
3452
|
+
startMonitoring(pluginName, plugin) {
|
|
3453
|
+
const config = this.healthChecks.get(pluginName);
|
|
3454
|
+
if (!config) {
|
|
3455
|
+
this.logger.warn("Cannot start monitoring - plugin not registered", { plugin: pluginName });
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
this.stopMonitoring(pluginName);
|
|
3459
|
+
const interval = setInterval(() => {
|
|
3460
|
+
this.performHealthCheck(pluginName, plugin, config).catch((error) => {
|
|
3461
|
+
this.logger.error("Health check failed with error", {
|
|
3462
|
+
plugin: pluginName,
|
|
3463
|
+
error
|
|
3464
|
+
});
|
|
3465
|
+
});
|
|
3466
|
+
}, config.interval);
|
|
3467
|
+
this.checkIntervals.set(pluginName, interval);
|
|
3468
|
+
this.logger.info("Health monitoring started", { plugin: pluginName });
|
|
3469
|
+
this.performHealthCheck(pluginName, plugin, config).catch((error) => {
|
|
3470
|
+
this.logger.error("Initial health check failed", {
|
|
3471
|
+
plugin: pluginName,
|
|
3472
|
+
error
|
|
3473
|
+
});
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Stop monitoring a plugin
|
|
3478
|
+
*/
|
|
3479
|
+
stopMonitoring(pluginName) {
|
|
3480
|
+
const interval = this.checkIntervals.get(pluginName);
|
|
3481
|
+
if (interval) {
|
|
3482
|
+
clearInterval(interval);
|
|
3483
|
+
this.checkIntervals.delete(pluginName);
|
|
3484
|
+
this.logger.info("Health monitoring stopped", { plugin: pluginName });
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Perform a health check on a plugin
|
|
3489
|
+
*/
|
|
3490
|
+
async performHealthCheck(pluginName, plugin, config) {
|
|
3491
|
+
const startTime = Date.now();
|
|
3492
|
+
let status = "healthy";
|
|
3493
|
+
let message;
|
|
3494
|
+
const checks = [];
|
|
3495
|
+
try {
|
|
3496
|
+
if (config.checkMethod && typeof plugin[config.checkMethod] === "function") {
|
|
3497
|
+
const checkResult = await Promise.race([
|
|
3498
|
+
plugin[config.checkMethod](),
|
|
3499
|
+
this.timeout(config.timeout, `Health check timeout after ${config.timeout}ms`)
|
|
3500
|
+
]);
|
|
3501
|
+
if (checkResult === false || checkResult && checkResult.status === "unhealthy") {
|
|
3502
|
+
status = "unhealthy";
|
|
3503
|
+
message = checkResult?.message || "Custom health check failed";
|
|
3504
|
+
checks.push({ name: config.checkMethod, status: "failed", message });
|
|
3505
|
+
} else {
|
|
3506
|
+
checks.push({ name: config.checkMethod, status: "passed" });
|
|
3507
|
+
}
|
|
3508
|
+
} else {
|
|
3509
|
+
checks.push({ name: "plugin-loaded", status: "passed" });
|
|
3510
|
+
}
|
|
3511
|
+
if (status === "healthy") {
|
|
3512
|
+
this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
|
|
3513
|
+
this.failureCounters.set(pluginName, 0);
|
|
3514
|
+
const currentStatus = this.healthStatus.get(pluginName);
|
|
3515
|
+
if (currentStatus === "unhealthy" || currentStatus === "degraded") {
|
|
3516
|
+
const successCount = this.successCounters.get(pluginName) || 0;
|
|
3517
|
+
if (successCount >= config.successThreshold) {
|
|
3518
|
+
this.healthStatus.set(pluginName, "healthy");
|
|
3519
|
+
this.logger.info("Plugin recovered to healthy state", { plugin: pluginName });
|
|
3520
|
+
} else {
|
|
3521
|
+
this.healthStatus.set(pluginName, "recovering");
|
|
3522
|
+
}
|
|
3523
|
+
} else {
|
|
3524
|
+
this.healthStatus.set(pluginName, "healthy");
|
|
3525
|
+
}
|
|
3526
|
+
} else {
|
|
3527
|
+
this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
|
|
3528
|
+
this.successCounters.set(pluginName, 0);
|
|
3529
|
+
const failureCount = this.failureCounters.get(pluginName) || 0;
|
|
3530
|
+
if (failureCount >= config.failureThreshold) {
|
|
3531
|
+
this.healthStatus.set(pluginName, "unhealthy");
|
|
3532
|
+
this.logger.warn("Plugin marked as unhealthy", {
|
|
3533
|
+
plugin: pluginName,
|
|
3534
|
+
failures: failureCount
|
|
3535
|
+
});
|
|
3536
|
+
if (config.autoRestart) {
|
|
3537
|
+
await this.attemptRestart(pluginName, plugin, config);
|
|
3538
|
+
}
|
|
3539
|
+
} else {
|
|
3540
|
+
this.healthStatus.set(pluginName, "degraded");
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
} catch (error) {
|
|
3544
|
+
status = "failed";
|
|
3545
|
+
message = error instanceof Error ? error.message : "Unknown error";
|
|
3546
|
+
this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
|
|
3547
|
+
this.healthStatus.set(pluginName, "failed");
|
|
3548
|
+
checks.push({
|
|
3549
|
+
name: "health-check",
|
|
3550
|
+
status: "failed",
|
|
3551
|
+
message
|
|
3552
|
+
});
|
|
3553
|
+
this.logger.error("Health check exception", {
|
|
3554
|
+
plugin: pluginName,
|
|
3555
|
+
error
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
const report = {
|
|
3559
|
+
status: this.healthStatus.get(pluginName) || "unknown",
|
|
3560
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3561
|
+
message,
|
|
3562
|
+
metrics: {
|
|
3563
|
+
uptime: Date.now() - startTime
|
|
3564
|
+
},
|
|
3565
|
+
checks: checks.length > 0 ? checks : void 0
|
|
3566
|
+
};
|
|
3567
|
+
this.healthReports.set(pluginName, report);
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Attempt to restart a plugin
|
|
3571
|
+
*/
|
|
3572
|
+
async attemptRestart(pluginName, plugin, config) {
|
|
3573
|
+
const attempts = this.restartAttempts.get(pluginName) || 0;
|
|
3574
|
+
if (attempts >= config.maxRestartAttempts) {
|
|
3575
|
+
this.logger.error("Max restart attempts reached, giving up", {
|
|
3576
|
+
plugin: pluginName,
|
|
3577
|
+
attempts
|
|
3578
|
+
});
|
|
3579
|
+
this.healthStatus.set(pluginName, "failed");
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
this.restartAttempts.set(pluginName, attempts + 1);
|
|
3583
|
+
const delay = this.calculateBackoff(attempts, config.restartBackoff);
|
|
3584
|
+
this.logger.info("Scheduling plugin restart", {
|
|
3585
|
+
plugin: pluginName,
|
|
3586
|
+
attempt: attempts + 1,
|
|
3587
|
+
delay
|
|
3588
|
+
});
|
|
3589
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3590
|
+
try {
|
|
3591
|
+
if (plugin.destroy) {
|
|
3592
|
+
await plugin.destroy();
|
|
3593
|
+
}
|
|
3594
|
+
this.logger.info("Plugin restarted", { plugin: pluginName });
|
|
3595
|
+
this.failureCounters.set(pluginName, 0);
|
|
3596
|
+
this.successCounters.set(pluginName, 0);
|
|
3597
|
+
this.healthStatus.set(pluginName, "recovering");
|
|
3598
|
+
} catch (error) {
|
|
3599
|
+
this.logger.error("Plugin restart failed", {
|
|
3600
|
+
plugin: pluginName,
|
|
3601
|
+
error
|
|
3602
|
+
});
|
|
3603
|
+
this.healthStatus.set(pluginName, "failed");
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
/**
|
|
3607
|
+
* Calculate backoff delay for restarts
|
|
3608
|
+
*/
|
|
3609
|
+
calculateBackoff(attempt, strategy) {
|
|
3610
|
+
const baseDelay = 1e3;
|
|
3611
|
+
switch (strategy) {
|
|
3612
|
+
case "fixed":
|
|
3613
|
+
return baseDelay;
|
|
3614
|
+
case "linear":
|
|
3615
|
+
return baseDelay * (attempt + 1);
|
|
3616
|
+
case "exponential":
|
|
3617
|
+
return baseDelay * Math.pow(2, attempt);
|
|
3618
|
+
default:
|
|
3619
|
+
return baseDelay;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* Get current health status of a plugin
|
|
3624
|
+
*/
|
|
3625
|
+
getHealthStatus(pluginName) {
|
|
3626
|
+
return this.healthStatus.get(pluginName);
|
|
3627
|
+
}
|
|
3628
|
+
/**
|
|
3629
|
+
* Get latest health report for a plugin
|
|
3630
|
+
*/
|
|
3631
|
+
getHealthReport(pluginName) {
|
|
3632
|
+
return this.healthReports.get(pluginName);
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Get all health statuses
|
|
3636
|
+
*/
|
|
3637
|
+
getAllHealthStatuses() {
|
|
3638
|
+
return new Map(this.healthStatus);
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Shutdown health monitor
|
|
3642
|
+
*/
|
|
3643
|
+
shutdown() {
|
|
3644
|
+
for (const pluginName of this.checkIntervals.keys()) {
|
|
3645
|
+
this.stopMonitoring(pluginName);
|
|
3646
|
+
}
|
|
3647
|
+
this.healthChecks.clear();
|
|
3648
|
+
this.healthStatus.clear();
|
|
3649
|
+
this.healthReports.clear();
|
|
3650
|
+
this.failureCounters.clear();
|
|
3651
|
+
this.successCounters.clear();
|
|
3652
|
+
this.restartAttempts.clear();
|
|
3653
|
+
this.logger.info("Health monitor shutdown complete");
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Timeout helper
|
|
3657
|
+
*/
|
|
3658
|
+
timeout(ms, message) {
|
|
3659
|
+
return new Promise((_, reject) => {
|
|
3660
|
+
setTimeout(() => reject(new Error(message)), ms);
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
};
|
|
3664
|
+
|
|
3665
|
+
// src/hot-reload.ts
|
|
3666
|
+
var generateUUID = () => {
|
|
3667
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
3668
|
+
return crypto.randomUUID();
|
|
3669
|
+
}
|
|
3670
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
|
3671
|
+
const r = Math.random() * 16 | 0;
|
|
3672
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
3673
|
+
return v.toString(16);
|
|
3674
|
+
});
|
|
3675
|
+
};
|
|
3676
|
+
var PluginStateManager = class {
|
|
3677
|
+
constructor(logger) {
|
|
3678
|
+
this.stateSnapshots = /* @__PURE__ */ new Map();
|
|
3679
|
+
this.memoryStore = /* @__PURE__ */ new Map();
|
|
3680
|
+
this.logger = logger.child({ component: "StateManager" });
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Save plugin state before reload
|
|
3684
|
+
*/
|
|
3685
|
+
async saveState(pluginId, version, state, config) {
|
|
3686
|
+
const snapshot = {
|
|
3687
|
+
pluginId,
|
|
3688
|
+
version,
|
|
3689
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3690
|
+
state,
|
|
3691
|
+
metadata: {
|
|
3692
|
+
checksum: this.calculateChecksum(state),
|
|
3693
|
+
compressed: false
|
|
3694
|
+
}
|
|
3695
|
+
};
|
|
3696
|
+
const snapshotId = generateUUID();
|
|
3697
|
+
switch (config.stateStrategy) {
|
|
3698
|
+
case "memory":
|
|
3699
|
+
this.memoryStore.set(snapshotId, snapshot);
|
|
3700
|
+
this.logger.debug("State saved to memory", { pluginId, snapshotId });
|
|
3701
|
+
break;
|
|
3702
|
+
case "disk":
|
|
3703
|
+
this.memoryStore.set(snapshotId, snapshot);
|
|
3704
|
+
this.logger.debug("State saved to disk (memory fallback)", { pluginId, snapshotId });
|
|
3705
|
+
break;
|
|
3706
|
+
case "distributed":
|
|
3707
|
+
this.memoryStore.set(snapshotId, snapshot);
|
|
3708
|
+
this.logger.debug("State saved to distributed store (memory fallback)", {
|
|
3709
|
+
pluginId,
|
|
3710
|
+
snapshotId
|
|
3711
|
+
});
|
|
3712
|
+
break;
|
|
3713
|
+
case "none":
|
|
3714
|
+
this.logger.debug("State persistence disabled", { pluginId });
|
|
3715
|
+
break;
|
|
3716
|
+
}
|
|
3717
|
+
this.stateSnapshots.set(pluginId, snapshot);
|
|
3718
|
+
return snapshotId;
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Restore plugin state after reload
|
|
3722
|
+
*/
|
|
3723
|
+
async restoreState(pluginId, snapshotId) {
|
|
3724
|
+
let snapshot;
|
|
3725
|
+
if (snapshotId) {
|
|
3726
|
+
snapshot = this.memoryStore.get(snapshotId);
|
|
3727
|
+
} else {
|
|
3728
|
+
snapshot = this.stateSnapshots.get(pluginId);
|
|
3729
|
+
}
|
|
3730
|
+
if (!snapshot) {
|
|
3731
|
+
this.logger.warn("No state snapshot found", { pluginId, snapshotId });
|
|
3732
|
+
return void 0;
|
|
3733
|
+
}
|
|
3734
|
+
if (snapshot.metadata?.checksum) {
|
|
3735
|
+
const currentChecksum = this.calculateChecksum(snapshot.state);
|
|
3736
|
+
if (currentChecksum !== snapshot.metadata.checksum) {
|
|
3737
|
+
this.logger.error("State checksum mismatch - data may be corrupted", {
|
|
3738
|
+
pluginId,
|
|
3739
|
+
expected: snapshot.metadata.checksum,
|
|
3740
|
+
actual: currentChecksum
|
|
3741
|
+
});
|
|
3742
|
+
return void 0;
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
this.logger.debug("State restored", { pluginId, version: snapshot.version });
|
|
3746
|
+
return snapshot.state;
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Clear state for a plugin
|
|
3750
|
+
*/
|
|
3751
|
+
clearState(pluginId) {
|
|
3752
|
+
this.stateSnapshots.delete(pluginId);
|
|
3753
|
+
this.logger.debug("State cleared", { pluginId });
|
|
3754
|
+
}
|
|
3755
|
+
/**
|
|
3756
|
+
* Calculate simple checksum for state verification
|
|
3757
|
+
* WARNING: This is a simple hash for demo purposes.
|
|
3758
|
+
* In production, use a cryptographic hash like SHA-256.
|
|
3759
|
+
*/
|
|
3760
|
+
calculateChecksum(state) {
|
|
3761
|
+
const stateStr = JSON.stringify(state);
|
|
3762
|
+
let hash = 0;
|
|
3763
|
+
for (let i = 0; i < stateStr.length; i++) {
|
|
3764
|
+
const char = stateStr.charCodeAt(i);
|
|
3765
|
+
hash = (hash << 5) - hash + char;
|
|
3766
|
+
hash = hash & hash;
|
|
3767
|
+
}
|
|
3768
|
+
return hash.toString(16);
|
|
3769
|
+
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Shutdown state manager
|
|
3772
|
+
*/
|
|
3773
|
+
shutdown() {
|
|
3774
|
+
this.stateSnapshots.clear();
|
|
3775
|
+
this.memoryStore.clear();
|
|
3776
|
+
this.logger.info("State manager shutdown complete");
|
|
3777
|
+
}
|
|
3778
|
+
};
|
|
3779
|
+
var HotReloadManager = class {
|
|
3780
|
+
constructor(logger) {
|
|
3781
|
+
this.reloadConfigs = /* @__PURE__ */ new Map();
|
|
3782
|
+
this.watchHandles = /* @__PURE__ */ new Map();
|
|
3783
|
+
this.reloadTimers = /* @__PURE__ */ new Map();
|
|
3784
|
+
this.logger = logger.child({ component: "HotReload" });
|
|
3785
|
+
this.stateManager = new PluginStateManager(logger);
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Register a plugin for hot reload
|
|
3789
|
+
*/
|
|
3790
|
+
registerPlugin(pluginName, config) {
|
|
3791
|
+
if (!config.enabled) {
|
|
3792
|
+
this.logger.debug("Hot reload disabled for plugin", { plugin: pluginName });
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
this.reloadConfigs.set(pluginName, config);
|
|
3796
|
+
this.logger.info("Plugin registered for hot reload", {
|
|
3797
|
+
plugin: pluginName,
|
|
3798
|
+
watchPatterns: config.watchPatterns,
|
|
3799
|
+
stateStrategy: config.stateStrategy
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Start watching for changes (requires file system integration)
|
|
3804
|
+
*/
|
|
3805
|
+
startWatching(pluginName) {
|
|
3806
|
+
const config = this.reloadConfigs.get(pluginName);
|
|
3807
|
+
if (!config || !config.enabled) {
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
this.logger.info("File watching started", {
|
|
3811
|
+
plugin: pluginName,
|
|
3812
|
+
patterns: config.watchPatterns
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
/**
|
|
3816
|
+
* Stop watching for changes
|
|
3817
|
+
*/
|
|
3818
|
+
stopWatching(pluginName) {
|
|
3819
|
+
const handle = this.watchHandles.get(pluginName);
|
|
3820
|
+
if (handle) {
|
|
3821
|
+
this.watchHandles.delete(pluginName);
|
|
3822
|
+
this.logger.info("File watching stopped", { plugin: pluginName });
|
|
3823
|
+
}
|
|
3824
|
+
const timer = this.reloadTimers.get(pluginName);
|
|
3825
|
+
if (timer) {
|
|
3826
|
+
clearTimeout(timer);
|
|
3827
|
+
this.reloadTimers.delete(pluginName);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Trigger hot reload for a plugin
|
|
3832
|
+
*/
|
|
3833
|
+
async reloadPlugin(pluginName, plugin, version, getPluginState, restorePluginState) {
|
|
3834
|
+
const config = this.reloadConfigs.get(pluginName);
|
|
3835
|
+
if (!config) {
|
|
3836
|
+
this.logger.warn("Cannot reload - plugin not registered", { plugin: pluginName });
|
|
3837
|
+
return false;
|
|
3838
|
+
}
|
|
3839
|
+
this.logger.info("Starting hot reload", { plugin: pluginName });
|
|
3840
|
+
try {
|
|
3841
|
+
if (config.beforeReload) {
|
|
3842
|
+
this.logger.debug("Executing before reload hooks", {
|
|
3843
|
+
plugin: pluginName,
|
|
3844
|
+
hooks: config.beforeReload
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
let snapshotId;
|
|
3848
|
+
if (config.preserveState && config.stateStrategy !== "none") {
|
|
3849
|
+
const state = getPluginState();
|
|
3850
|
+
snapshotId = await this.stateManager.saveState(
|
|
3851
|
+
pluginName,
|
|
3852
|
+
version,
|
|
3853
|
+
state,
|
|
3854
|
+
config
|
|
3855
|
+
);
|
|
3856
|
+
this.logger.debug("Plugin state saved", { plugin: pluginName, snapshotId });
|
|
3857
|
+
}
|
|
3858
|
+
if (plugin.destroy) {
|
|
3859
|
+
this.logger.debug("Destroying plugin", { plugin: pluginName });
|
|
3860
|
+
const shutdownPromise = plugin.destroy();
|
|
3861
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3862
|
+
setTimeout(() => reject(new Error("Shutdown timeout")), config.shutdownTimeout);
|
|
3863
|
+
});
|
|
3864
|
+
await Promise.race([shutdownPromise, timeoutPromise]);
|
|
3865
|
+
this.logger.debug("Plugin destroyed successfully", { plugin: pluginName });
|
|
3866
|
+
}
|
|
3867
|
+
this.logger.debug("Plugin module would be reloaded here", { plugin: pluginName });
|
|
3868
|
+
if (snapshotId && config.preserveState) {
|
|
3869
|
+
const restoredState = await this.stateManager.restoreState(pluginName, snapshotId);
|
|
3870
|
+
if (restoredState) {
|
|
3871
|
+
restorePluginState(restoredState);
|
|
3872
|
+
this.logger.debug("Plugin state restored", { plugin: pluginName });
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
if (config.afterReload) {
|
|
3876
|
+
this.logger.debug("Executing after reload hooks", {
|
|
3877
|
+
plugin: pluginName,
|
|
3878
|
+
hooks: config.afterReload
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
this.logger.info("Hot reload completed successfully", { plugin: pluginName });
|
|
3882
|
+
return true;
|
|
3883
|
+
} catch (error) {
|
|
3884
|
+
this.logger.error("Hot reload failed", {
|
|
3885
|
+
plugin: pluginName,
|
|
3886
|
+
error
|
|
3887
|
+
});
|
|
3888
|
+
return false;
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
/**
|
|
3892
|
+
* Schedule a reload with debouncing
|
|
3893
|
+
*/
|
|
3894
|
+
scheduleReload(pluginName, reloadFn) {
|
|
3895
|
+
const config = this.reloadConfigs.get(pluginName);
|
|
3896
|
+
if (!config) {
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
const existingTimer = this.reloadTimers.get(pluginName);
|
|
3900
|
+
if (existingTimer) {
|
|
3901
|
+
clearTimeout(existingTimer);
|
|
3902
|
+
}
|
|
3903
|
+
const timer = setTimeout(() => {
|
|
3904
|
+
this.logger.debug("Debounce period elapsed, executing reload", {
|
|
3905
|
+
plugin: pluginName
|
|
3906
|
+
});
|
|
3907
|
+
reloadFn().catch((error) => {
|
|
3908
|
+
this.logger.error("Scheduled reload failed", {
|
|
3909
|
+
plugin: pluginName,
|
|
3910
|
+
error
|
|
3911
|
+
});
|
|
3912
|
+
});
|
|
3913
|
+
this.reloadTimers.delete(pluginName);
|
|
3914
|
+
}, config.debounceDelay);
|
|
3915
|
+
this.reloadTimers.set(pluginName, timer);
|
|
3916
|
+
this.logger.debug("Reload scheduled with debounce", {
|
|
3917
|
+
plugin: pluginName,
|
|
3918
|
+
delay: config.debounceDelay
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Get state manager for direct access
|
|
3923
|
+
*/
|
|
3924
|
+
getStateManager() {
|
|
3925
|
+
return this.stateManager;
|
|
3926
|
+
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Shutdown hot reload manager
|
|
3929
|
+
*/
|
|
3930
|
+
shutdown() {
|
|
3931
|
+
for (const pluginName of this.watchHandles.keys()) {
|
|
3932
|
+
this.stopWatching(pluginName);
|
|
3933
|
+
}
|
|
3934
|
+
for (const timer of this.reloadTimers.values()) {
|
|
3935
|
+
clearTimeout(timer);
|
|
3936
|
+
}
|
|
3937
|
+
this.reloadConfigs.clear();
|
|
3938
|
+
this.watchHandles.clear();
|
|
3939
|
+
this.reloadTimers.clear();
|
|
3940
|
+
this.stateManager.shutdown();
|
|
3941
|
+
this.logger.info("Hot reload manager shutdown complete");
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
|
|
3945
|
+
// src/dependency-resolver.ts
|
|
3946
|
+
var SemanticVersionManager = class {
|
|
3947
|
+
/**
|
|
3948
|
+
* Parse a version string into semantic version components
|
|
3949
|
+
*/
|
|
3950
|
+
static parse(versionStr) {
|
|
3951
|
+
const cleanVersion = versionStr.replace(/^v/, "");
|
|
3952
|
+
const match = cleanVersion.match(
|
|
3953
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/
|
|
3954
|
+
);
|
|
3955
|
+
if (!match) {
|
|
3956
|
+
throw new Error(`Invalid semantic version: ${versionStr}`);
|
|
3957
|
+
}
|
|
3958
|
+
return {
|
|
3959
|
+
major: parseInt(match[1], 10),
|
|
3960
|
+
minor: parseInt(match[2], 10),
|
|
3961
|
+
patch: parseInt(match[3], 10),
|
|
3962
|
+
preRelease: match[4],
|
|
3963
|
+
build: match[5]
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Convert semantic version back to string
|
|
3968
|
+
*/
|
|
3969
|
+
static toString(version) {
|
|
3970
|
+
let str = `${version.major}.${version.minor}.${version.patch}`;
|
|
3971
|
+
if (version.preRelease) {
|
|
3972
|
+
str += `-${version.preRelease}`;
|
|
3973
|
+
}
|
|
3974
|
+
if (version.build) {
|
|
3975
|
+
str += `+${version.build}`;
|
|
3976
|
+
}
|
|
3977
|
+
return str;
|
|
3978
|
+
}
|
|
3979
|
+
/**
|
|
3980
|
+
* Compare two semantic versions
|
|
3981
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
3982
|
+
*/
|
|
3983
|
+
static compare(a, b) {
|
|
3984
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
3985
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
3986
|
+
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
3987
|
+
if (a.preRelease && !b.preRelease) return -1;
|
|
3988
|
+
if (!a.preRelease && b.preRelease) return 1;
|
|
3989
|
+
if (a.preRelease && b.preRelease) {
|
|
3990
|
+
return a.preRelease.localeCompare(b.preRelease);
|
|
3991
|
+
}
|
|
3992
|
+
return 0;
|
|
3993
|
+
}
|
|
3994
|
+
/**
|
|
3995
|
+
* Check if version satisfies constraint
|
|
3996
|
+
*/
|
|
3997
|
+
static satisfies(version, constraint) {
|
|
3998
|
+
const constraintStr = constraint;
|
|
3999
|
+
if (constraintStr === "*" || constraintStr === "latest") {
|
|
4000
|
+
return true;
|
|
4001
|
+
}
|
|
4002
|
+
if (/^[\d.]+$/.test(constraintStr)) {
|
|
4003
|
+
const exact = this.parse(constraintStr);
|
|
4004
|
+
return this.compare(version, exact) === 0;
|
|
4005
|
+
}
|
|
4006
|
+
if (constraintStr.startsWith("^")) {
|
|
4007
|
+
const base = this.parse(constraintStr.slice(1));
|
|
4008
|
+
return version.major === base.major && this.compare(version, base) >= 0;
|
|
4009
|
+
}
|
|
4010
|
+
if (constraintStr.startsWith("~")) {
|
|
4011
|
+
const base = this.parse(constraintStr.slice(1));
|
|
4012
|
+
return version.major === base.major && version.minor === base.minor && this.compare(version, base) >= 0;
|
|
4013
|
+
}
|
|
4014
|
+
if (constraintStr.startsWith(">=")) {
|
|
4015
|
+
const base = this.parse(constraintStr.slice(2));
|
|
4016
|
+
return this.compare(version, base) >= 0;
|
|
4017
|
+
}
|
|
4018
|
+
if (constraintStr.startsWith(">")) {
|
|
4019
|
+
const base = this.parse(constraintStr.slice(1));
|
|
4020
|
+
return this.compare(version, base) > 0;
|
|
4021
|
+
}
|
|
4022
|
+
if (constraintStr.startsWith("<=")) {
|
|
4023
|
+
const base = this.parse(constraintStr.slice(2));
|
|
4024
|
+
return this.compare(version, base) <= 0;
|
|
4025
|
+
}
|
|
4026
|
+
if (constraintStr.startsWith("<")) {
|
|
4027
|
+
const base = this.parse(constraintStr.slice(1));
|
|
4028
|
+
return this.compare(version, base) < 0;
|
|
4029
|
+
}
|
|
4030
|
+
const rangeMatch = constraintStr.match(/^([\d.]+)\s*-\s*([\d.]+)$/);
|
|
4031
|
+
if (rangeMatch) {
|
|
4032
|
+
const min = this.parse(rangeMatch[1]);
|
|
4033
|
+
const max = this.parse(rangeMatch[2]);
|
|
4034
|
+
return this.compare(version, min) >= 0 && this.compare(version, max) <= 0;
|
|
4035
|
+
}
|
|
4036
|
+
return false;
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Determine compatibility level between two versions
|
|
4040
|
+
*/
|
|
4041
|
+
static getCompatibilityLevel(from, to) {
|
|
4042
|
+
const cmp = this.compare(from, to);
|
|
4043
|
+
if (cmp === 0) {
|
|
4044
|
+
return "fully-compatible";
|
|
4045
|
+
}
|
|
4046
|
+
if (from.major !== to.major) {
|
|
4047
|
+
return "breaking-changes";
|
|
4048
|
+
}
|
|
4049
|
+
if (from.minor < to.minor) {
|
|
4050
|
+
return "backward-compatible";
|
|
4051
|
+
}
|
|
4052
|
+
if (from.patch < to.patch) {
|
|
4053
|
+
return "fully-compatible";
|
|
4054
|
+
}
|
|
4055
|
+
return "incompatible";
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4058
|
+
var DependencyResolver = class {
|
|
4059
|
+
constructor(logger) {
|
|
4060
|
+
this.logger = logger.child({ component: "DependencyResolver" });
|
|
4061
|
+
}
|
|
4062
|
+
/**
|
|
4063
|
+
* Resolve dependencies using topological sort
|
|
4064
|
+
*/
|
|
4065
|
+
resolve(plugins) {
|
|
4066
|
+
const graph = /* @__PURE__ */ new Map();
|
|
4067
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
4068
|
+
for (const [pluginName, pluginInfo] of plugins) {
|
|
4069
|
+
if (!graph.has(pluginName)) {
|
|
4070
|
+
graph.set(pluginName, []);
|
|
4071
|
+
inDegree.set(pluginName, 0);
|
|
4072
|
+
}
|
|
4073
|
+
const deps = pluginInfo.dependencies || [];
|
|
4074
|
+
for (const dep of deps) {
|
|
4075
|
+
if (!plugins.has(dep)) {
|
|
4076
|
+
throw new Error(`Missing dependency: ${pluginName} requires ${dep}`);
|
|
4077
|
+
}
|
|
4078
|
+
if (!graph.has(dep)) {
|
|
4079
|
+
graph.set(dep, []);
|
|
4080
|
+
inDegree.set(dep, 0);
|
|
4081
|
+
}
|
|
4082
|
+
graph.get(dep).push(pluginName);
|
|
4083
|
+
inDegree.set(pluginName, (inDegree.get(pluginName) || 0) + 1);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
const queue = [];
|
|
4087
|
+
const result = [];
|
|
4088
|
+
for (const [node, degree] of inDegree) {
|
|
4089
|
+
if (degree === 0) {
|
|
4090
|
+
queue.push(node);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
while (queue.length > 0) {
|
|
4094
|
+
const node = queue.shift();
|
|
4095
|
+
result.push(node);
|
|
4096
|
+
const dependents = graph.get(node) || [];
|
|
4097
|
+
for (const dependent of dependents) {
|
|
4098
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
4099
|
+
inDegree.set(dependent, newDegree);
|
|
4100
|
+
if (newDegree === 0) {
|
|
4101
|
+
queue.push(dependent);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
if (result.length !== plugins.size) {
|
|
4106
|
+
const remaining = Array.from(plugins.keys()).filter((p) => !result.includes(p));
|
|
4107
|
+
this.logger.error("Circular dependency detected", { remaining });
|
|
4108
|
+
throw new Error(`Circular dependency detected among: ${remaining.join(", ")}`);
|
|
4109
|
+
}
|
|
4110
|
+
this.logger.debug("Dependencies resolved", { order: result });
|
|
4111
|
+
return result;
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Detect dependency conflicts
|
|
4115
|
+
*/
|
|
4116
|
+
detectConflicts(plugins) {
|
|
4117
|
+
const conflicts = [];
|
|
4118
|
+
const versionRequirements = /* @__PURE__ */ new Map();
|
|
4119
|
+
for (const [pluginName, pluginInfo] of plugins) {
|
|
4120
|
+
if (!pluginInfo.dependencies) continue;
|
|
4121
|
+
for (const [depName, constraint] of Object.entries(pluginInfo.dependencies)) {
|
|
4122
|
+
if (!versionRequirements.has(depName)) {
|
|
4123
|
+
versionRequirements.set(depName, /* @__PURE__ */ new Map());
|
|
4124
|
+
}
|
|
4125
|
+
versionRequirements.get(depName).set(pluginName, constraint);
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
for (const [depName, requirements] of versionRequirements) {
|
|
4129
|
+
const depInfo = plugins.get(depName);
|
|
4130
|
+
if (!depInfo) continue;
|
|
4131
|
+
const depVersion = SemanticVersionManager.parse(depInfo.version);
|
|
4132
|
+
const unsatisfied = [];
|
|
4133
|
+
for (const [requiringPlugin, constraint] of requirements) {
|
|
4134
|
+
if (!SemanticVersionManager.satisfies(depVersion, constraint)) {
|
|
4135
|
+
unsatisfied.push({
|
|
4136
|
+
pluginId: requiringPlugin,
|
|
4137
|
+
version: constraint
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
if (unsatisfied.length > 0) {
|
|
4142
|
+
conflicts.push({
|
|
4143
|
+
type: "version-mismatch",
|
|
4144
|
+
severity: "error",
|
|
4145
|
+
description: `Version mismatch for ${depName}: detected ${unsatisfied.length} unsatisfied requirements`,
|
|
4146
|
+
plugins: [
|
|
4147
|
+
{ pluginId: depName, version: depInfo.version },
|
|
4148
|
+
...unsatisfied
|
|
4149
|
+
],
|
|
4150
|
+
resolutions: [{
|
|
4151
|
+
strategy: "upgrade",
|
|
4152
|
+
description: `Upgrade ${depName} to satisfy all constraints`,
|
|
4153
|
+
targetPlugins: [depName],
|
|
4154
|
+
automatic: false
|
|
4155
|
+
}]
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
try {
|
|
4160
|
+
this.resolve(new Map(
|
|
4161
|
+
Array.from(plugins.entries()).map(([name, info]) => [
|
|
4162
|
+
name,
|
|
4163
|
+
{ version: info.version, dependencies: info.dependencies ? Object.keys(info.dependencies) : [] }
|
|
4164
|
+
])
|
|
4165
|
+
));
|
|
4166
|
+
} catch (error) {
|
|
4167
|
+
if (error instanceof Error && error.message.includes("Circular dependency")) {
|
|
4168
|
+
conflicts.push({
|
|
4169
|
+
type: "circular-dependency",
|
|
4170
|
+
severity: "critical",
|
|
4171
|
+
description: error.message,
|
|
4172
|
+
plugins: [],
|
|
4173
|
+
// Would need to extract from error
|
|
4174
|
+
resolutions: [{
|
|
4175
|
+
strategy: "manual",
|
|
4176
|
+
description: "Remove circular dependency by restructuring plugins",
|
|
4177
|
+
automatic: false
|
|
4178
|
+
}]
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
return conflicts;
|
|
4183
|
+
}
|
|
4184
|
+
/**
|
|
4185
|
+
* Find best version that satisfies all constraints
|
|
4186
|
+
*/
|
|
4187
|
+
findBestVersion(availableVersions, constraints) {
|
|
4188
|
+
const versions = availableVersions.map((v) => ({ str: v, parsed: SemanticVersionManager.parse(v) })).sort((a, b) => -SemanticVersionManager.compare(a.parsed, b.parsed));
|
|
4189
|
+
for (const version of versions) {
|
|
4190
|
+
const satisfiesAll = constraints.every(
|
|
4191
|
+
(constraint) => SemanticVersionManager.satisfies(version.parsed, constraint)
|
|
4192
|
+
);
|
|
4193
|
+
if (satisfiesAll) {
|
|
4194
|
+
return version.str;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return void 0;
|
|
4198
|
+
}
|
|
4199
|
+
/**
|
|
4200
|
+
* Check if dependencies form a valid DAG (no cycles)
|
|
4201
|
+
*/
|
|
4202
|
+
isAcyclic(dependencies) {
|
|
4203
|
+
try {
|
|
4204
|
+
const plugins = new Map(
|
|
4205
|
+
Array.from(dependencies.entries()).map(([name, deps]) => [
|
|
4206
|
+
name,
|
|
4207
|
+
{ dependencies: deps }
|
|
4208
|
+
])
|
|
4209
|
+
);
|
|
4210
|
+
this.resolve(plugins);
|
|
4211
|
+
return true;
|
|
4212
|
+
} catch {
|
|
4213
|
+
return false;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
export {
|
|
4218
|
+
ApiRegistry,
|
|
4219
|
+
DependencyResolver,
|
|
4220
|
+
HotReloadManager,
|
|
4221
|
+
LiteKernel,
|
|
4222
|
+
ObjectKernel,
|
|
4223
|
+
ObjectKernelBase,
|
|
4224
|
+
ObjectLogger,
|
|
4225
|
+
PluginConfigValidator,
|
|
4226
|
+
PluginHealthMonitor,
|
|
4227
|
+
PluginLoader,
|
|
4228
|
+
PluginPermissionEnforcer,
|
|
4229
|
+
PluginPermissionManager,
|
|
4230
|
+
PluginSandboxRuntime,
|
|
4231
|
+
PluginSecurityScanner,
|
|
4232
|
+
PluginSignatureVerifier,
|
|
4233
|
+
qa_exports as QA,
|
|
4234
|
+
SecurePluginContext,
|
|
4235
|
+
SemanticVersionManager,
|
|
4236
|
+
ServiceLifecycle,
|
|
4237
|
+
createApiRegistryPlugin,
|
|
4238
|
+
createLogger,
|
|
4239
|
+
createPluginConfigValidator,
|
|
4240
|
+
createPluginPermissionEnforcer,
|
|
4241
|
+
getEnv,
|
|
4242
|
+
getMemoryUsage,
|
|
4243
|
+
isNode,
|
|
4244
|
+
safeExit
|
|
4245
|
+
};
|
|
4246
|
+
//# sourceMappingURL=index.js.map
|