@africode/core 5.0.0 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENT_SCHEMA.json +837 -0
- package/components/base.d.ts +1 -1
- package/components/base.js +71 -21
- package/core/a2ui-schema-manager.js +9 -2
- package/core/a2ui.js +131 -43
- package/core/actions.js +27 -0
- package/core/bun-runtime.js +207 -724
- package/core/compliance.js +6 -5
- package/core/config.js +7 -5
- package/core/enhanced-hmr.js +16 -14
- package/core/file-router.js +42 -282
- package/core/hmr.js +8 -7
- package/core/html.d.ts +15 -101
- package/core/html.js +53 -129
- package/core/lipa-namba-journey.js +72 -61
- package/core/logging.js +14 -0
- package/core/middleware.js +82 -0
- package/core/nida-cig-middleware.js +13 -8
- package/core/plugins/index.js +345 -312
- package/core/request-identity.js +44 -0
- package/core/sdk.js +22 -0
- package/core/session-store.js +68 -0
- package/core/state.js +34 -0
- package/core/websocket.js +22 -20
- package/dist/africode.js +108 -112
- package/dist/africode.js.map +6 -6
- package/dist/build-info.json +3 -3
- package/dist/components.js +351 -351
- package/dist/components.js.map +6 -6
- package/package.json +6 -4
- package/scripts/generate-component-schema.js +80 -0
package/core/plugins/index.js
CHANGED
|
@@ -1,312 +1,345 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AfriCode Plugin Registry
|
|
3
|
-
*
|
|
4
|
-
* A minimal, strict, and predictable plug-in architecture for the AfriCode Framework.
|
|
5
|
-
* Employs deterministic execution, strong boundary isolation, and a stable context
|
|
6
|
-
* contract to prevent ecosystem bloat and ensure framework resilience.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Core framework version to check plugin compatibility
|
|
10
|
-
export const AFRICODE_VERSION = '
|
|
11
|
-
|
|
12
|
-
// Only these specific hooks are permitted in v1
|
|
13
|
-
export const ALLOWED_HOOKS = new Set([
|
|
14
|
-
'onConfigLoad',
|
|
15
|
-
'onComponentRegister',
|
|
16
|
-
'onRouteRegister',
|
|
17
|
-
'onStateInit',
|
|
18
|
-
'onServerStart',
|
|
19
|
-
'onRequest',
|
|
20
|
-
'onResponse',
|
|
21
|
-
'onError',
|
|
22
|
-
'onCliCommandRegister',
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
// Protected core CLI commands that plugins cannot overwrite
|
|
26
|
-
const PROTECTED_CLI_COMMANDS = new Set([
|
|
27
|
-
'dev',
|
|
28
|
-
'build',
|
|
29
|
-
'start',
|
|
30
|
-
'test',
|
|
31
|
-
'lint',
|
|
32
|
-
'audit',
|
|
33
|
-
'add',
|
|
34
|
-
'migrate',
|
|
35
|
-
'create',
|
|
36
|
-
'help',
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
export class AfriPluginRegistry {
|
|
40
|
-
constructor(options = {}) {
|
|
41
|
-
this.plugins = new Map();
|
|
42
|
-
// Mode mapping: safe (swallow errors), strict (propagate crashes immediately)
|
|
43
|
-
this.mode = options.mode || 'safe';
|
|
44
|
-
|
|
45
|
-
// Plugin state tracking
|
|
46
|
-
this.disabledPlugins = new Set();
|
|
47
|
-
|
|
48
|
-
// Internal structured store for deterministic hook execution (arrays maintain registration order)
|
|
49
|
-
this.hooks = {
|
|
50
|
-
onConfigLoad: [],
|
|
51
|
-
onComponentRegister: [],
|
|
52
|
-
onRouteRegister: [],
|
|
53
|
-
onStateInit: [],
|
|
54
|
-
onServerStart: [],
|
|
55
|
-
onRequest: [],
|
|
56
|
-
onResponse: [],
|
|
57
|
-
onError: [],
|
|
58
|
-
onCliCommandRegister: [],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Store plugin-registered CLI commands
|
|
62
|
-
this.pluginCommands = new Map();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Registers a new plugin into the framework.
|
|
67
|
-
* @param {Object} manifest - The Plugin manifest
|
|
68
|
-
* @param {Object} hooks - The hooks implementation
|
|
69
|
-
*/
|
|
70
|
-
register(manifest, hooks) {
|
|
71
|
-
this._validateManifest(manifest);
|
|
72
|
-
this._validateCompatibility(manifest.compatibleWith);
|
|
73
|
-
this._validateHooks(manifest.name, hooks);
|
|
74
|
-
|
|
75
|
-
if (this.plugins.has(manifest.name)) {
|
|
76
|
-
console.warn(`[AfriCode Plugin] Plugin '${manifest.name}' is already registered. Skipping.`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this.plugins.set(manifest.name, manifest);
|
|
81
|
-
|
|
82
|
-
// Bind hooks into the deterministic execution pipeline
|
|
83
|
-
for (const [hookName, handler] of Object.entries(hooks)) {
|
|
84
|
-
this.hooks[hookName].push({
|
|
85
|
-
pluginName: manifest.name,
|
|
86
|
-
handler,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Observability Hook
|
|
91
|
-
this._logObservability('onPluginLoad', manifest.name, `Registered v${manifest.version}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Disable a plugin at runtime. Safely ejects it from the pipeline.
|
|
96
|
-
*/
|
|
97
|
-
disable(pluginName) {
|
|
98
|
-
if (!this.plugins.has(pluginName)) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
this.disabledPlugins.add(pluginName);
|
|
102
|
-
this._logObservability('onPluginDisable', pluginName, 'Plugin forcefully disabled.');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Enable a previously disabled plugin.
|
|
107
|
-
*/
|
|
108
|
-
enable(pluginName) {
|
|
109
|
-
this.disabledPlugins.delete(pluginName);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Plugin Registry
|
|
3
|
+
*
|
|
4
|
+
* A minimal, strict, and predictable plug-in architecture for the AfriCode Framework.
|
|
5
|
+
* Employs deterministic execution, strong boundary isolation, and a stable context
|
|
6
|
+
* contract to prevent ecosystem bloat and ensure framework resilience.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Core framework version to check plugin compatibility
|
|
10
|
+
export const AFRICODE_VERSION = '5.0.0';
|
|
11
|
+
|
|
12
|
+
// Only these specific hooks are permitted in v1
|
|
13
|
+
export const ALLOWED_HOOKS = new Set([
|
|
14
|
+
'onConfigLoad',
|
|
15
|
+
'onComponentRegister',
|
|
16
|
+
'onRouteRegister',
|
|
17
|
+
'onStateInit',
|
|
18
|
+
'onServerStart',
|
|
19
|
+
'onRequest',
|
|
20
|
+
'onResponse',
|
|
21
|
+
'onError',
|
|
22
|
+
'onCliCommandRegister',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// Protected core CLI commands that plugins cannot overwrite
|
|
26
|
+
const PROTECTED_CLI_COMMANDS = new Set([
|
|
27
|
+
'dev',
|
|
28
|
+
'build',
|
|
29
|
+
'start',
|
|
30
|
+
'test',
|
|
31
|
+
'lint',
|
|
32
|
+
'audit',
|
|
33
|
+
'add',
|
|
34
|
+
'migrate',
|
|
35
|
+
'create',
|
|
36
|
+
'help',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export class AfriPluginRegistry {
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.plugins = new Map();
|
|
42
|
+
// Mode mapping: safe (swallow errors), strict (propagate crashes immediately)
|
|
43
|
+
this.mode = options.mode || 'safe';
|
|
44
|
+
|
|
45
|
+
// Plugin state tracking
|
|
46
|
+
this.disabledPlugins = new Set();
|
|
47
|
+
|
|
48
|
+
// Internal structured store for deterministic hook execution (arrays maintain registration order)
|
|
49
|
+
this.hooks = {
|
|
50
|
+
onConfigLoad: [],
|
|
51
|
+
onComponentRegister: [],
|
|
52
|
+
onRouteRegister: [],
|
|
53
|
+
onStateInit: [],
|
|
54
|
+
onServerStart: [],
|
|
55
|
+
onRequest: [],
|
|
56
|
+
onResponse: [],
|
|
57
|
+
onError: [],
|
|
58
|
+
onCliCommandRegister: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Store plugin-registered CLI commands
|
|
62
|
+
this.pluginCommands = new Map();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registers a new plugin into the framework.
|
|
67
|
+
* @param {Object} manifest - The Plugin manifest
|
|
68
|
+
* @param {Object} hooks - The hooks implementation
|
|
69
|
+
*/
|
|
70
|
+
register(manifest, hooks) {
|
|
71
|
+
this._validateManifest(manifest);
|
|
72
|
+
this._validateCompatibility(manifest.compatibleWith);
|
|
73
|
+
this._validateHooks(manifest.name, hooks);
|
|
74
|
+
|
|
75
|
+
if (this.plugins.has(manifest.name)) {
|
|
76
|
+
console.warn(`[AfriCode Plugin] Plugin '${manifest.name}' is already registered. Skipping.`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.plugins.set(manifest.name, manifest);
|
|
81
|
+
|
|
82
|
+
// Bind hooks into the deterministic execution pipeline
|
|
83
|
+
for (const [hookName, handler] of Object.entries(hooks)) {
|
|
84
|
+
this.hooks[hookName].push({
|
|
85
|
+
pluginName: manifest.name,
|
|
86
|
+
handler,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Observability Hook
|
|
91
|
+
this._logObservability('onPluginLoad', manifest.name, `Registered v${manifest.version}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Disable a plugin at runtime. Safely ejects it from the pipeline.
|
|
96
|
+
*/
|
|
97
|
+
disable(pluginName) {
|
|
98
|
+
if (!this.plugins.has(pluginName)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.disabledPlugins.add(pluginName);
|
|
102
|
+
this._logObservability('onPluginDisable', pluginName, 'Plugin forcefully disabled.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Enable a previously disabled plugin.
|
|
107
|
+
*/
|
|
108
|
+
enable(pluginName) {
|
|
109
|
+
this.disabledPlugins.delete(pluginName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns a stable, public snapshot of the plugin registry for docs/debugging.
|
|
114
|
+
*/
|
|
115
|
+
getSnapshot() {
|
|
116
|
+
return {
|
|
117
|
+
frameworkVersion: AFRICODE_VERSION,
|
|
118
|
+
mode: this.mode,
|
|
119
|
+
plugins: [...this.plugins.values()].map((manifest) => ({
|
|
120
|
+
name: manifest.name,
|
|
121
|
+
version: manifest.version,
|
|
122
|
+
compatibleWith: manifest.compatibleWith,
|
|
123
|
+
hooks: Array.isArray(manifest.hooks) ? [...manifest.hooks] : [],
|
|
124
|
+
disabled: this.disabledPlugins.has(manifest.name)
|
|
125
|
+
})),
|
|
126
|
+
allowedHooks: [...ALLOWED_HOOKS],
|
|
127
|
+
protectedCliCommands: [...PROTECTED_CLI_COMMANDS]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns the canonical plugin lifecycle description.
|
|
133
|
+
*/
|
|
134
|
+
getLifecycleGuide() {
|
|
135
|
+
return [
|
|
136
|
+
{ step: 'manifest', description: 'Validate name, version, and compatibleWith.' },
|
|
137
|
+
{ step: 'hooks', description: 'Allow only approved hook names and function handlers.' },
|
|
138
|
+
{ step: 'register', description: 'Bind hooks in registration order.' },
|
|
139
|
+
{ step: 'emit', description: 'Execute with a restricted stable context.' },
|
|
140
|
+
{ step: 'disable', description: 'Skip disabled plugins without mutating the rest.' },
|
|
141
|
+
{ step: 'strict-mode', description: 'Propagate failures immediately when configured.' }
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Execute a specific hook safely across all registered plugins in registration order.
|
|
147
|
+
* @param {string} hookName - The name of the hook to trigger
|
|
148
|
+
* @param {Object} context - A stable execution context exposed to plugins
|
|
149
|
+
*/
|
|
150
|
+
async emit(hookName, context = {}) {
|
|
151
|
+
if (!ALLOWED_HOOKS.has(hookName)) {
|
|
152
|
+
throw new Error(`[AfriCode Plugin] Attempted to emit unknown hook: ${hookName}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handlers = this.hooks[hookName];
|
|
156
|
+
if (!handlers || handlers.length === 0) {
|
|
157
|
+
return context;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Clone context defensively to prevent deep framework mutation
|
|
161
|
+
// but allow mutations on specifically designed properties.
|
|
162
|
+
const stableContext = this._buildStableContext(hookName, context);
|
|
163
|
+
|
|
164
|
+
for (const { pluginName, handler } of handlers) {
|
|
165
|
+
if (this.disabledPlugins.has(pluginName)) {
|
|
166
|
+
continue;
|
|
167
|
+
} // Skip disabled plugins
|
|
168
|
+
|
|
169
|
+
const hookPromise = async () => {
|
|
170
|
+
const startTime = performance.now();
|
|
171
|
+
await handler(stableContext);
|
|
172
|
+
const latency = performance.now() - startTime;
|
|
173
|
+
|
|
174
|
+
// Deep trace for heavy hooks taking >50ms
|
|
175
|
+
if (latency > 50) {
|
|
176
|
+
this._logObservability(
|
|
177
|
+
'onHookComplete',
|
|
178
|
+
pluginName,
|
|
179
|
+
`Hook ${hookName} resolved in ${latency.toFixed(2)}ms`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
185
|
+
setTimeout(() => reject(new Error(`[Timeout] Plugin Execution exceeded 200ms`)), 200)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Execution Guarantee: Plugin must resolve within 200ms boundary
|
|
190
|
+
await Promise.race([hookPromise(), timeoutPromise]);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this._logObservability(
|
|
193
|
+
'onPluginError',
|
|
194
|
+
pluginName,
|
|
195
|
+
`Failed during '${hookName}': ${error.message}`
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Isolation Policy: Bubble crash explicitly if registry runs in 'strict' mode
|
|
199
|
+
if (this.mode === 'strict') {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Safe mode: Prevent crash mapping downstream
|
|
204
|
+
if (hookName !== 'onError') {
|
|
205
|
+
await this.emit('onError', {
|
|
206
|
+
error,
|
|
207
|
+
source: `plugin:${pluginName}`,
|
|
208
|
+
hook: hookName,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return stableContext;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Internal observability tracking logger to format structured output guarantees
|
|
219
|
+
*/
|
|
220
|
+
_logObservability(event, pluginName, metadata) {
|
|
221
|
+
const timestamp = new Date().toISOString();
|
|
222
|
+
console.log(
|
|
223
|
+
`[AfriCode Observability] ${timestamp} | EVENT: ${event} | PLUGIN: ${pluginName} | ${metadata}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validates the schema of a plugin manifest.
|
|
229
|
+
*/
|
|
230
|
+
_validateManifest(manifest) {
|
|
231
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
232
|
+
throw new Error('[AfriCode Plugin] Plugin manifest must be a valid object.');
|
|
233
|
+
}
|
|
234
|
+
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
235
|
+
throw new Error("[AfriCode Plugin] Plugin manifest is missing a valid 'name' property.");
|
|
236
|
+
}
|
|
237
|
+
if (!manifest.version || typeof manifest.version !== 'string') {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`[AfriCode Plugin: ${manifest.name}] Manifest is missing a valid 'version' property.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!manifest.compatibleWith) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`[AfriCode Plugin: ${manifest.name}] Manifest is missing 'compatibleWith' boundary property.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Optional lightweight boundary check against standard semantic mapping.
|
|
251
|
+
*/
|
|
252
|
+
_validateCompatibility(compatibleWith) {
|
|
253
|
+
// In a full production scenario, use a semantic versioning library.
|
|
254
|
+
// For v1, we provide a structured warning if versions mismatched wildly.
|
|
255
|
+
if (typeof compatibleWith !== 'string') {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Very basic extraction of major version target (e.g., ^2.0.0 or ~3.0.0)
|
|
260
|
+
const targetMajor = compatibleWith.match(/\d+/)?.[0];
|
|
261
|
+
const currentMajor = AFRICODE_VERSION.split('.')[0];
|
|
262
|
+
|
|
263
|
+
if (targetMajor && targetMajor !== currentMajor) {
|
|
264
|
+
console.warn(
|
|
265
|
+
`[AfriCode Plugin Warning] Plugin requires compatibility '${compatibleWith}' but framework is v${AFRICODE_VERSION}. Unexpected behavior may occur.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Ensures plugins only utilize globally permitted v1 hooks.
|
|
272
|
+
*/
|
|
273
|
+
_validateHooks(pluginName, hooks) {
|
|
274
|
+
if (!hooks || typeof hooks !== 'object') {
|
|
275
|
+
throw new Error(`[AfriCode Plugin: ${pluginName}] Hooks object is missing or invalid.`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const hookName of Object.keys(hooks)) {
|
|
279
|
+
if (!ALLOWED_HOOKS.has(hookName)) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`[AfriCode Plugin: ${pluginName}] Invalid hook attempt: '${hookName}'. Authorized hooks are explicitly scoped.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (typeof hooks[hookName] !== 'function') {
|
|
285
|
+
throw new Error(`[AfriCode Plugin: ${pluginName}] Hook '${hookName}' must be a function.`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Scopes the contextual payload based on the specific hook type.
|
|
292
|
+
* Prevents plugins from pulling down random core internals.
|
|
293
|
+
*/
|
|
294
|
+
_buildStableContext(hookName, payload = {}) {
|
|
295
|
+
switch (hookName) {
|
|
296
|
+
case 'onCliCommandRegister':
|
|
297
|
+
return {
|
|
298
|
+
// Safe command registration mechanism
|
|
299
|
+
registerCommand: (commandName, commandHandler) => {
|
|
300
|
+
if (PROTECTED_CLI_COMMANDS.has(commandName)) {
|
|
301
|
+
console.warn(
|
|
302
|
+
`[AfriCode Plugin] Refused to overwrite core framework CLI command: '${commandName}'.`
|
|
303
|
+
);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
this.pluginCommands.set(commandName, commandHandler);
|
|
307
|
+
return true;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
case 'onRouteRegister':
|
|
311
|
+
return {
|
|
312
|
+
addRoute: payload.addRoute, // The framework passes a strict routing interface
|
|
313
|
+
router: payload.router,
|
|
314
|
+
};
|
|
315
|
+
case 'onServerStart':
|
|
316
|
+
return { port: payload.port };
|
|
317
|
+
case 'onStateInit':
|
|
318
|
+
return { store: payload.store };
|
|
319
|
+
case 'onComponentRegister':
|
|
320
|
+
return { componentName: payload.name, componentClass: payload.component };
|
|
321
|
+
case 'onRequest':
|
|
322
|
+
case 'onResponse':
|
|
323
|
+
// Plugins receive specific restricted request/response maps
|
|
324
|
+
return { req: payload.req, res: payload.res, meta: payload.meta || {} };
|
|
325
|
+
case 'onError':
|
|
326
|
+
return { error: payload.error, source: payload.source };
|
|
327
|
+
case 'onConfigLoad': {
|
|
328
|
+
// Security Hardening: Never dump RAW process.env unconditionally
|
|
329
|
+
// Mask sensitive keys by delivering a tailored subset mapping.
|
|
330
|
+
const env = typeof process !== 'undefined' ? process.env : {};
|
|
331
|
+
const safeEnvSubset = {
|
|
332
|
+
NODE_ENV: env.NODE_ENV || 'development',
|
|
333
|
+
PORT: env.PORT || 3000,
|
|
334
|
+
// Blacklist database IPs, JWT secrets, Stripe Keys from rogue downstream plugins
|
|
335
|
+
};
|
|
336
|
+
return { config: payload.config, env: safeEnvSubset };
|
|
337
|
+
}
|
|
338
|
+
default:
|
|
339
|
+
return payload;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Global static registry instance for core
|
|
345
|
+
export const registry = new AfriPluginRegistry();
|