@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.
@@ -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 = '4.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
- * Execute a specific hook safely across all registered plugins in registration order.
114
- * @param {string} hookName - The name of the hook to trigger
115
- * @param {Object} context - A stable execution context exposed to plugins
116
- */
117
- async emit(hookName, context = {}) {
118
- if (!ALLOWED_HOOKS.has(hookName)) {
119
- throw new Error(`[AfriCode Plugin] Attempted to emit unknown hook: ${hookName}`);
120
- }
121
-
122
- const handlers = this.hooks[hookName];
123
- if (!handlers || handlers.length === 0) {
124
- return context;
125
- }
126
-
127
- // Clone context defensively to prevent deep framework mutation
128
- // but allow mutations on specifically designed properties.
129
- const stableContext = this._buildStableContext(hookName, context);
130
-
131
- for (const { pluginName, handler } of handlers) {
132
- if (this.disabledPlugins.has(pluginName)) {
133
- continue;
134
- } // Skip disabled plugins
135
-
136
- const hookPromise = async () => {
137
- const startTime = performance.now();
138
- await handler(stableContext);
139
- const latency = performance.now() - startTime;
140
-
141
- // Deep trace for heavy hooks taking >50ms
142
- if (latency > 50) {
143
- this._logObservability(
144
- 'onHookComplete',
145
- pluginName,
146
- `Hook ${hookName} resolved in ${latency.toFixed(2)}ms`
147
- );
148
- }
149
- };
150
-
151
- const timeoutPromise = new Promise((_, reject) =>
152
- setTimeout(() => reject(new Error(`[Timeout] Plugin Execution exceeded 200ms`)), 200)
153
- );
154
-
155
- try {
156
- // Execution Guarantee: Plugin must resolve within 200ms boundary
157
- await Promise.race([hookPromise(), timeoutPromise]);
158
- } catch (error) {
159
- this._logObservability(
160
- 'onPluginError',
161
- pluginName,
162
- `Failed during '${hookName}': ${error.message}`
163
- );
164
-
165
- // Isolation Policy: Bubble crash explicitly if registry runs in 'strict' mode
166
- if (this.mode === 'strict') {
167
- throw error;
168
- }
169
-
170
- // Safe mode: Prevent crash mapping downstream
171
- if (hookName !== 'onError') {
172
- await this.emit('onError', {
173
- error,
174
- source: `plugin:${pluginName}`,
175
- hook: hookName,
176
- });
177
- }
178
- }
179
- }
180
-
181
- return stableContext;
182
- }
183
-
184
- /**
185
- * Internal observability tracking logger to format structured output guarantees
186
- */
187
- _logObservability(event, pluginName, metadata) {
188
- const timestamp = new Date().toISOString();
189
- console.log(
190
- `[AfriCode Observability] ${timestamp} | EVENT: ${event} | PLUGIN: ${pluginName} | ${metadata}`
191
- );
192
- }
193
-
194
- /**
195
- * Validates the schema of a plugin manifest.
196
- */
197
- _validateManifest(manifest) {
198
- if (!manifest || typeof manifest !== 'object') {
199
- throw new Error('[AfriCode Plugin] Plugin manifest must be a valid object.');
200
- }
201
- if (!manifest.name || typeof manifest.name !== 'string') {
202
- throw new Error("[AfriCode Plugin] Plugin manifest is missing a valid 'name' property.");
203
- }
204
- if (!manifest.version || typeof manifest.version !== 'string') {
205
- throw new Error(
206
- `[AfriCode Plugin: ${manifest.name}] Manifest is missing a valid 'version' property.`
207
- );
208
- }
209
- if (!manifest.compatibleWith) {
210
- throw new Error(
211
- `[AfriCode Plugin: ${manifest.name}] Manifest is missing 'compatibleWith' boundary property.`
212
- );
213
- }
214
- }
215
-
216
- /**
217
- * Optional lightweight boundary check against standard semantic mapping.
218
- */
219
- _validateCompatibility(compatibleWith) {
220
- // In a full production scenario, use a semantic versioning library.
221
- // For v1, we provide a structured warning if versions mismatched wildly.
222
- if (typeof compatibleWith !== 'string') {
223
- return;
224
- }
225
-
226
- // Very basic extraction of major version target (e.g., ^2.0.0 or ~3.0.0)
227
- const targetMajor = compatibleWith.match(/\d+/)?.[0];
228
- const currentMajor = AFRICODE_VERSION.split('.')[0];
229
-
230
- if (targetMajor && targetMajor !== currentMajor) {
231
- console.warn(
232
- `[AfriCode Plugin Warning] Plugin requires compatibility '${compatibleWith}' but framework is v${AFRICODE_VERSION}. Unexpected behavior may occur.`
233
- );
234
- }
235
- }
236
-
237
- /**
238
- * Ensures plugins only utilize globally permitted v1 hooks.
239
- */
240
- _validateHooks(pluginName, hooks) {
241
- if (!hooks || typeof hooks !== 'object') {
242
- throw new Error(`[AfriCode Plugin: ${pluginName}] Hooks object is missing or invalid.`);
243
- }
244
-
245
- for (const hookName of Object.keys(hooks)) {
246
- if (!ALLOWED_HOOKS.has(hookName)) {
247
- throw new Error(
248
- `[AfriCode Plugin: ${pluginName}] Invalid hook attempt: '${hookName}'. Authorized hooks are explicitly scoped.`
249
- );
250
- }
251
- if (typeof hooks[hookName] !== 'function') {
252
- throw new Error(`[AfriCode Plugin: ${pluginName}] Hook '${hookName}' must be a function.`);
253
- }
254
- }
255
- }
256
-
257
- /**
258
- * Scopes the contextual payload based on the specific hook type.
259
- * Prevents plugins from pulling down random core internals.
260
- */
261
- _buildStableContext(hookName, payload = {}) {
262
- switch (hookName) {
263
- case 'onCliCommandRegister':
264
- return {
265
- // Safe command registration mechanism
266
- registerCommand: (commandName, commandHandler) => {
267
- if (PROTECTED_CLI_COMMANDS.has(commandName)) {
268
- console.warn(
269
- `[AfriCode Plugin] Refused to overwrite core framework CLI command: '${commandName}'.`
270
- );
271
- return false;
272
- }
273
- this.pluginCommands.set(commandName, commandHandler);
274
- return true;
275
- },
276
- };
277
- case 'onRouteRegister':
278
- return {
279
- addRoute: payload.addRoute, // The framework passes a strict routing interface
280
- router: payload.router,
281
- };
282
- case 'onServerStart':
283
- return { port: payload.port };
284
- case 'onStateInit':
285
- return { store: payload.store };
286
- case 'onComponentRegister':
287
- return { componentName: payload.name, componentClass: payload.component };
288
- case 'onRequest':
289
- case 'onResponse':
290
- // Plugins receive specific restricted request/response maps
291
- return { req: payload.req, res: payload.res, meta: payload.meta || {} };
292
- case 'onError':
293
- return { error: payload.error, source: payload.source };
294
- case 'onConfigLoad': {
295
- // Security Hardening: Never dump RAW process.env unconditionally
296
- // Mask sensitive keys by delivering a tailored subset mapping.
297
- const env = typeof process !== 'undefined' ? process.env : {};
298
- const safeEnvSubset = {
299
- NODE_ENV: env.NODE_ENV || 'development',
300
- PORT: env.PORT || 3000,
301
- // Blacklist database IPs, JWT secrets, Stripe Keys from rogue downstream plugins
302
- };
303
- return { config: payload.config, env: safeEnvSubset };
304
- }
305
- default:
306
- return payload;
307
- }
308
- }
309
- }
310
-
311
- // Global static registry instance for core
312
- export const registry = new AfriPluginRegistry();
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();