@africode/core 5.0.8 → 5.0.9

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.
Files changed (62) hide show
  1. package/AGENT_INSTRUCTIONS.md +595 -595
  2. package/COMPONENT_SCHEMA.json +1800 -991
  3. package/bin/create-africode.js +87 -25
  4. package/components/auth-form.js +154 -0
  5. package/components/index.js +10 -0
  6. package/components/kyc-upload.js +173 -0
  7. package/components/nav-drawer.js +217 -0
  8. package/components/transaction-ledger.js +138 -0
  9. package/components/wallet-balance.js +114 -0
  10. package/core/a2ui-schema-manager.js +1 -1
  11. package/core/a2ui.js +178 -1
  12. package/core/bun-runtime.js +122 -7
  13. package/core/cli/commands/build.js +30 -5
  14. package/core/cli/ui.js +13 -3
  15. package/core/compliance.js +201 -0
  16. package/core/middleware.js +80 -17
  17. package/core/patterns.js +168 -0
  18. package/core/request-analytics.js +254 -0
  19. package/core/request-identity.js +79 -29
  20. package/core/sdk.js +4 -1
  21. package/core/validation.js +13 -0
  22. package/dist/africode.js +858 -457
  23. package/dist/africode.js.map +14 -9
  24. package/dist/build-info.json +3 -3
  25. package/dist/components.js +784 -383
  26. package/dist/components.js.map +11 -6
  27. package/package.json +1 -1
  28. package/templates/starter/package.json +5 -5
  29. package/templates/starter/src/index.js +18 -0
  30. package/templates/starter/src/pages/index.js +1 -1
  31. package/templates/starter-3d/package.json +5 -5
  32. package/templates/starter-3d/src/pages/index.js +1 -1
  33. package/templates/starter-dashboard/.env.example +21 -0
  34. package/templates/starter-dashboard/africode.config.js +20 -0
  35. package/templates/starter-dashboard/package.json +14 -0
  36. package/templates/starter-dashboard/src/index.js +17 -0
  37. package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
  38. package/templates/starter-dashboard/src/pages/index.html +118 -0
  39. package/templates/starter-dashboard/src/pages/index.js +110 -0
  40. package/templates/starter-dashboard/src/styles/main.css +172 -0
  41. package/templates/starter-fintech/.env.example +28 -0
  42. package/templates/starter-fintech/africode.config.js +20 -0
  43. package/templates/starter-fintech/package.json +15 -0
  44. package/templates/starter-fintech/src/index.js +17 -0
  45. package/templates/starter-fintech/src/pages/api/auth.js +45 -0
  46. package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
  47. package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
  48. package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
  49. package/templates/starter-fintech/src/pages/index.html +169 -0
  50. package/templates/starter-fintech/src/pages/index.js +161 -0
  51. package/templates/starter-fintech/src/styles/main.css +246 -0
  52. package/templates/starter-react/package.json +5 -5
  53. package/templates/starter-react/src/pages/index.js +1 -1
  54. package/templates/starter-tailwind/package.json +5 -5
  55. package/templates/starter-tailwind/src/pages/index.js +1 -1
  56. package/templates/starter-website/.env.example +18 -0
  57. package/templates/starter-website/africode.config.js +20 -0
  58. package/templates/starter-website/package.json +14 -0
  59. package/templates/starter-website/src/index.js +17 -0
  60. package/templates/starter-website/src/pages/index.html +124 -0
  61. package/templates/starter-website/src/pages/index.js +116 -0
  62. package/templates/starter-website/src/styles/main.css +195 -0
@@ -13,7 +13,9 @@ import { actions } from './actions.js';
13
13
  import { MiddlewareManager, loggerMiddleware } from './middleware.js';
14
14
  import { EnhancedHMRMiddleware } from './enhanced-hmr.js';
15
15
  import { Database as BunSQLiteDatabase } from 'bun:sqlite';
16
- import { extname, join } from 'path';
16
+ import { existsSync } from 'fs';
17
+ import { extname, join, resolve } from 'path';
18
+ import { pathToFileURL } from 'url';
17
19
 
18
20
  export const BUN_CONFIG = {
19
21
  startupTimeout: 5000,
@@ -24,14 +26,98 @@ export const BUN_CONFIG = {
24
26
 
25
27
  export class BunHTTPServer {
26
28
  constructor(options = {}) {
27
- this.router = new FileSystemRouter(join(process.cwd(), options.pagesDir || 'pages'));
28
-
29
+ this.projectRoot = resolve(options.projectRoot || process.cwd());
30
+ this.explicitPagesDir = Boolean(options.pagesDir);
31
+ this.explicitPort = options.port !== undefined;
32
+ this.configPath = options.configPath === false
33
+ ? null
34
+ : resolve(this.projectRoot, options.configPath || 'africode.config.js');
35
+ this.config = options.config || {};
36
+ this.configLoaded = Boolean(options.config || options.configPath === false);
29
37
  this.middleware = new MiddlewareManager();
30
- this.port = options.port || 3000;
38
+ this.configMiddlewareRegistered = false;
39
+ this.port = options.port || this.config.server?.port || 3000;
31
40
  this.server = null;
41
+
42
+ if (!this.configLoaded && !this.explicitPagesDir && this.configPath && existsSync(this.configPath)) {
43
+ this.pagesDir = null;
44
+ this.router = null;
45
+ } else {
46
+ this.configureRouter(options.pagesDir || this.config.directories?.pages || 'pages');
47
+ }
48
+ }
49
+
50
+ configureRouter(pagesDir) {
51
+ const absolutePagesDir = resolve(this.projectRoot, pagesDir || 'pages');
52
+ this.pagesDir = absolutePagesDir;
53
+ this.router = new FileSystemRouter(absolutePagesDir);
54
+ }
55
+
56
+ async loadProjectConfig() {
57
+ if (this.configLoaded) {
58
+ this.registerConfigMiddleware();
59
+ return this.config;
60
+ }
61
+
62
+ this.configLoaded = true;
63
+
64
+ if (!this.configPath || !existsSync(this.configPath)) {
65
+ this.registerConfigMiddleware();
66
+ return this.config;
67
+ }
68
+
69
+ const moduleUrl = pathToFileURL(this.configPath).href + '?t=' + Date.now();
70
+ const projectConfigModule = await import(moduleUrl);
71
+ const projectConfig = projectConfigModule.default || projectConfigModule.config || {};
72
+ this.config = { ...projectConfig, ...this.config };
73
+
74
+ if (!this.explicitPort && this.config.server?.port) {
75
+ this.port = Number(this.config.server.port) || this.port;
76
+ }
77
+
78
+ if (!this.explicitPagesDir) {
79
+ this.configureRouter(this.config.directories?.pages || this.config.pagesDir || 'pages');
80
+ }
81
+
82
+ this.registerConfigMiddleware();
83
+
84
+ return this.config;
85
+ }
86
+
87
+ getConfigMiddlewareList() {
88
+ const sources = [
89
+ this.config.middleware,
90
+ this.config.middlewares,
91
+ this.config.interceptors
92
+ ];
93
+
94
+ return sources
95
+ .flatMap((source) => {
96
+ if (!source) {
97
+ return [];
98
+ }
99
+
100
+ return Array.isArray(source) ? source : [source];
101
+ })
102
+ .filter(Boolean);
103
+ }
104
+
105
+ registerConfigMiddleware() {
106
+ if (this.configMiddlewareRegistered) {
107
+ return;
108
+ }
109
+
110
+ const middlewares = this.getConfigMiddlewareList();
111
+ for (const middleware of middlewares) {
112
+ this.middleware.use(middleware);
113
+ }
114
+
115
+ this.configMiddlewareRegistered = true;
32
116
  }
33
117
 
34
118
  async start() {
119
+ await this.loadProjectConfig();
120
+
35
121
  // Register middlewares
36
122
  this.middleware.use(loggerMiddleware());
37
123
 
@@ -55,6 +141,8 @@ export class BunHTTPServer {
55
141
  }
56
142
 
57
143
  async handleRequest(req) {
144
+ await this.loadProjectConfig();
145
+
58
146
  const url = new URL(req.url);
59
147
  const pathname = url.pathname;
60
148
  const sessionId = getRequestIdentity(req);
@@ -68,8 +156,16 @@ export class BunHTTPServer {
68
156
  let context = {
69
157
  req,
70
158
  url,
159
+ method: req.method,
160
+ headers: req.headers,
161
+ json: () => req.json(),
162
+ text: () => req.text(),
163
+ formData: () => req.formData(),
164
+ arrayBuffer: () => req.arrayBuffer(),
71
165
  pathname,
72
166
  params: {},
167
+ config: this.config,
168
+ projectRoot: this.projectRoot,
73
169
  sessionId,
74
170
  state: session,
75
171
  actions,
@@ -94,7 +190,8 @@ export class BunHTTPServer {
94
190
  pathname,
95
191
  filePath: route.filePath,
96
192
  isApi: route.isApi,
97
- isDynamic: route.isDynamic
193
+ isDynamic: route.isDynamic,
194
+ params: route.params || {}
98
195
  }
99
196
  };
100
197
 
@@ -113,7 +210,13 @@ export class BunHTTPServer {
113
210
  const handler = pageModule[method];
114
211
 
115
212
  if (!handler) {
116
- throw new Error("No " + method + " handler in API route");
213
+ const allowedMethods = this.getAllowedMethods(pageModule);
214
+ return this.attachSessionCookie(new Response('Method Not Allowed', {
215
+ status: 405,
216
+ headers: {
217
+ Allow: allowedMethods.join(', ')
218
+ }
219
+ }), sessionId);
117
220
  }
118
221
 
119
222
  const result = await handler(context);
@@ -134,6 +237,12 @@ export class BunHTTPServer {
134
237
  return this.attachSessionCookie(new Response('Route Error', { status: 500 }), sessionId);
135
238
  }
136
239
  }
240
+
241
+ getAllowedMethods(pageModule) {
242
+ const methodNames = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
243
+ return methodNames.filter((method) => typeof pageModule[method] === 'function');
244
+ }
245
+
137
246
  attachSessionCookie(response, sessionId) {
138
247
  const cookie = 'afri_session=' + sessionId + '; Path=/; HttpOnly; SameSite=Strict';
139
248
 
@@ -164,6 +273,12 @@ export class BunHTTPServer {
164
273
  });
165
274
  }
166
275
 
276
+ if (result && typeof result === 'object') {
277
+ return new Response(JSON.stringify(result), {
278
+ headers: { 'Content-Type': 'application/json' },
279
+ });
280
+ }
281
+
167
282
  // ❌ Invalid return type
168
283
  console.error('Invalid render output:', result);
169
284
 
@@ -176,7 +291,7 @@ export class BunHTTPServer {
176
291
  return new Response('Forbidden', { status: 403 });
177
292
  }
178
293
 
179
- const filePath = join(process.cwd(), pathname);
294
+ const filePath = join(this.projectRoot, pathname);
180
295
 
181
296
  try {
182
297
  const file = Bun.file(filePath);
@@ -7,6 +7,9 @@ import { existsSync } from 'fs';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { RawHtml } from '../../html.js';
10
+ import { actions } from '../../actions.js';
11
+ import { sessionStore } from '../../session-store.js';
12
+ import { getConfig } from '../../config.js';
10
13
 
11
14
  export async function normalizeRenderOutput(htmlContent) {
12
15
  if (htmlContent && typeof htmlContent.text === 'function') {
@@ -42,10 +45,13 @@ export async function build() {
42
45
 
43
46
  const distDir = './dist';
44
47
  const cwd = process.cwd();
45
- const hasSrc = existsSync(path.join(cwd, 'src'));
46
- const pagesDir = hasSrc ? './src/pages' : './pages';
47
- const componentsDir = hasSrc ? './src/components' : './components';
48
- const stylesDir = hasSrc ? './src/styles' : './styles';
48
+ const resolveProjectDir = (name) => {
49
+ const candidates = [`./src/${name}`, `./${name}`];
50
+ return candidates.find((candidate) => existsSync(path.join(cwd, candidate))) || candidates[0];
51
+ };
52
+ const pagesDir = resolveProjectDir('pages');
53
+ const componentsDir = resolveProjectDir('components');
54
+ const stylesDir = resolveProjectDir('styles');
49
55
 
50
56
  try {
51
57
  const buildStart = performance.now();
@@ -119,7 +125,25 @@ export async function build() {
119
125
  }
120
126
 
121
127
  if (typeof pageModule.default === 'function') {
122
- let htmlContent = await pageModule.default({ data: loaderData });
128
+ const sessionId = 'africode-ssg-build';
129
+ const requestUrl = new URL(`http://localhost${route}`);
130
+ const renderContext = {
131
+ data: loaderData,
132
+ req: new Request(requestUrl),
133
+ url: requestUrl,
134
+ method: 'GET',
135
+ headers: new Headers(),
136
+ pathname: route,
137
+ params: {},
138
+ config: getConfig(),
139
+ projectRoot: cwd,
140
+ sessionId,
141
+ state: sessionStore.get(sessionId),
142
+ actions,
143
+ sessionStore,
144
+ route: { path: route, params: {}, isDynamic: false, isApi: false }
145
+ };
146
+ let htmlContent = await pageModule.default(renderContext);
123
147
  htmlContent = await normalizeRenderOutput(htmlContent);
124
148
  const outputPath = path.join(distDir, `${name}.html`);
125
149
  await writeFile(outputPath, htmlContent);
@@ -164,5 +188,6 @@ export async function build() {
164
188
 
165
189
  } catch (err) {
166
190
  console.error(`${colors.red}✗ Build failed:${colors.reset}`, err.message || err);
191
+ throw err;
167
192
  }
168
193
  }
package/core/cli/ui.js CHANGED
@@ -31,14 +31,24 @@ export function help() {
31
31
  logo();
32
32
  console.log(`
33
33
  ${colors.bold}Usage:${colors.reset}
34
- africode <command> [options]
34
+ bun run ./bin/africode.js <command> [options]
35
+
36
+ ${colors.bold}Quick start:${colors.reset}
37
+ ${colors.green}bun install${colors.reset}
38
+ ${colors.green}bun run ./bin/africode.js create my-app${colors.reset}
39
+ ${colors.green}cd my-app${colors.reset}
40
+ ${colors.green}bun install${colors.reset}
41
+ ${colors.green}bun run ./bin/africode.js dev${colors.reset}
35
42
 
36
43
  ${colors.bold}Commands:${colors.reset}
37
44
  ${colors.green}dev${colors.reset} Start development server
38
45
  ${colors.green}build${colors.reset} Create production build
39
- ${colors.green}add${colors.reset} Add a component (e.g., africode add sidebar)
40
- ${colors.green}new${colors.reset} Create a new project (options: --template 3d)
46
+ ${colors.green}add${colors.reset} Add a component (e.g., bun run ./bin/africode.js add sidebar)
47
+ ${colors.green}create${colors.reset} Create a new project (use --template starter, tailwind, react, or 3d)
41
48
  ${colors.green}migrate${colors.reset} Run database migrations
49
+ ${colors.green}lint${colors.reset} Check project files for lint errors
50
+ ${colors.green}audit${colors.reset} Audit project structure and framework usage
51
+ ${colors.green}test${colors.reset} Run the project test suite
42
52
  ${colors.green}help${colors.reset} Show this help message
43
53
 
44
54
  ${colors.gold}Powered by the rhythm of the continent.${colors.reset}
@@ -462,6 +462,70 @@ export class AMLComplianceEngine {
462
462
  return flags;
463
463
  }
464
464
 
465
+ /**
466
+ * Enhanced risk scoring for African markets
467
+ */
468
+ async _calculateAfricanRiskScore(transaction, customer) {
469
+ let score = await this._calculateRiskScore(transaction, customer);
470
+
471
+ // Additional African-specific checks
472
+
473
+ // Check for transactions involving multiple African countries
474
+ if (transaction.crossBorder) {
475
+ score += 0.15;
476
+ }
477
+
478
+ // Check for cryptocurrency-related activities
479
+ if (transaction.paymentMethod?.includes('crypto')) {
480
+ score += 0.25;
481
+ }
482
+
483
+ // Check for mobile money transactions (common in Africa)
484
+ if (transaction.paymentMethod === 'mobile_money' && amount > 500000) { // 500k TZS
485
+ score += 0.1;
486
+ }
487
+
488
+ // Check for politically exposed persons (PEPs)
489
+ if (customer.isPEP) {
490
+ score += 0.3;
491
+ }
492
+
493
+ // Check for high-risk sectors common in Africa
494
+ if (customer.businessSector && ['casino', 'gambling', 'cash_intensive'].includes(customer.businessSector)) {
495
+ score += 0.2;
496
+ }
497
+
498
+ return Math.min(score, 1.0);
499
+ }
500
+
501
+ /**
502
+ * Multi-country sanctions screening
503
+ */
504
+ async screenSanctions(parties) {
505
+ // Screen against OFAC, UN, EU, and African Union sanctions lists
506
+ const results = [];
507
+
508
+ for (const party of parties) {
509
+ const sanctionsCheck = {
510
+ name: party.name,
511
+ id: party.id,
512
+ matchedLists: [],
513
+ riskLevel: 'low'
514
+ };
515
+
516
+ // In a real implementation, this would query sanctions databases
517
+ // This is a simplified version
518
+ if (party.name.toLowerCase().includes('sanctioned')) {
519
+ sanctionsCheck.matchedLists.push('internal_watchlist');
520
+ sanctionsCheck.riskLevel = 'high';
521
+ }
522
+
523
+ results.push(sanctionsCheck);
524
+ }
525
+
526
+ return results;
527
+ }
528
+
465
529
  /**
466
530
  * Queue suspicious activity for FIU reporting
467
531
  */
@@ -627,3 +691,140 @@ export class ComplianceMiddleware {
627
691
  };
628
692
  }
629
693
  }
694
+
695
+ /**
696
+ * African Payment Systems Compliance
697
+ * Integration with regional payment systems and compliance requirements
698
+ */
699
+ export class AfricanPaymentCompliance {
700
+ constructor(config = {}) {
701
+ this.supportedCountries = config.countries || ['TZ', 'KE', 'UG', 'RW', 'ZA'];
702
+ this.mobileMoneyProviders = config.providers || ['Mpesa', 'TigoPesa', 'AirtelMoney', 'MTNMobileMoney'];
703
+ this.currencyThresholds = config.thresholds || {
704
+ TZS: 1000000, // 1M TZS
705
+ KES: 50000, // 50K KES
706
+ UGX: 2000000, // 2M UGX
707
+ ZAR: 2500 // 2.5K ZAR
708
+ };
709
+ }
710
+
711
+ /**
712
+ * Validate mobile money transaction
713
+ * @param {Object} transaction - Transaction details
714
+ * @param {Object} sender - Sender profile
715
+ * @param {Object} receiver - Receiver profile
716
+ */
717
+ async validateMobileMoneyTransaction(transaction, sender, receiver) {
718
+ const validation = {
719
+ valid: true,
720
+ warnings: [],
721
+ errors: []
722
+ };
723
+
724
+ // Check if countries are supported
725
+ if (!this.supportedCountries.includes(transaction.senderCountry)) {
726
+ validation.errors.push('Sender country not supported');
727
+ validation.valid = false;
728
+ }
729
+
730
+ if (!this.supportedCountries.includes(transaction.receiverCountry)) {
731
+ validation.errors.push('Receiver country not supported');
732
+ validation.valid = false;
733
+ }
734
+
735
+ // Check if provider is supported
736
+ if (!this.mobileMoneyProviders.includes(transaction.provider)) {
737
+ validation.warnings.push('Provider not in standard list');
738
+ }
739
+
740
+ // Check amount thresholds
741
+ const threshold = this.currencyThresholds[transaction.currency];
742
+ if (threshold && transaction.amount > threshold) {
743
+ validation.warnings.push(`Amount exceeds recommended threshold for ${transaction.currency}`);
744
+
745
+ // May require additional verification
746
+ if (transaction.amount > threshold * 2) {
747
+ validation.warnings.push('Amount significantly above threshold - enhanced due diligence recommended');
748
+ }
749
+ }
750
+
751
+ // Check for frequent transactions (possible structuring)
752
+ if (sender.dailyTransactionCount > 10) {
753
+ validation.warnings.push('High frequency of transactions today');
754
+ }
755
+
756
+ return validation;
757
+ }
758
+
759
+ /**
760
+ * Generate compliance report for mobile money transactions
761
+ */
762
+ async generateMobileMoneyReport(transactions, period) {
763
+ const report = {
764
+ period,
765
+ totalTransactions: transactions.length,
766
+ totalVolume: 0,
767
+ byCountry: {},
768
+ flaggedTransactions: []
769
+ };
770
+
771
+ for (const tx of transactions) {
772
+ report.totalVolume += tx.amount;
773
+
774
+ // Group by country
775
+ if (!report.byCountry[tx.senderCountry]) {
776
+ report.byCountry[tx.senderCountry] = {
777
+ count: 0,
778
+ volume: 0
779
+ };
780
+ }
781
+
782
+ report.byCountry[tx.senderCountry].count++;
783
+ report.byCountry[tx.senderCountry].volume += tx.amount;
784
+
785
+ // Flag suspicious patterns
786
+ if (tx.amount > this.currencyThresholds[tx.currency] * 2) {
787
+ report.flaggedTransactions.push({
788
+ id: tx.id,
789
+ amount: tx.amount,
790
+ reason: 'High value transaction'
791
+ });
792
+ }
793
+ }
794
+
795
+ return report;
796
+ }
797
+
798
+ /**
799
+ * Cross-border transaction compliance
800
+ */
801
+ async validateCrossBorderTransaction(transaction) {
802
+ const validation = {
803
+ compliant: true,
804
+ requirements: [],
805
+ errors: []
806
+ };
807
+
808
+ // Check if transaction crosses borders
809
+ if (transaction.senderCountry !== transaction.receiverCountry) {
810
+ validation.requirements.push('cross_border_declaration');
811
+ validation.requirements.push('purpose_of_transfer');
812
+
813
+ // Higher scrutiny for cross-border
814
+ if (transaction.amount > 5000000) { // 5M TZS
815
+ validation.requirements.push('enhanced_verification');
816
+ validation.requirements.push('source_of_funds');
817
+ }
818
+ }
819
+
820
+ return validation;
821
+ }
822
+ }
823
+ export {
824
+ NIDAClient,
825
+ TIPSClient,
826
+ AMLComplianceEngine,
827
+ ComplianceMiddleware,
828
+ AfricanPaymentCompliance
829
+ };
830
+
@@ -6,41 +6,104 @@
6
6
  */
7
7
 
8
8
  import { frameworkLog } from './logging.js';
9
+ import { AfriCodeError } from './errors.js';
9
10
 
10
11
  export class MiddlewareManager {
11
12
  constructor() {
12
13
  this.middlewares = [];
14
+ this.isRunning = false;
13
15
  }
14
16
 
15
- use(fn) {
17
+ /**
18
+ * Add middleware to the pipeline
19
+ * @param {Function} fn - Middleware function
20
+ * @param {string} [name] - Optional name for debugging
21
+ * @returns {MiddlewareManager}
22
+ */
23
+ use(fn, name = null) {
16
24
  if (typeof fn !== 'function') {
17
- throw new Error('Middleware must be a function');
25
+ throw new AfriCodeError('Middleware must be a function', 'MIDDLEWARE_INVALID', {}, 500);
18
26
  }
19
27
 
20
- this.middlewares.push(fn);
28
+ // Wrap with name and error handling
29
+ const wrappedFn = async (context) => {
30
+ const startTime = Date.now();
31
+ try {
32
+ frameworkLog('debug', `[Middleware] Running ${name || 'anonymous'} middleware`);
33
+ const result = await fn(context);
34
+
35
+ const duration = Date.now() - startTime;
36
+ frameworkLog('debug', `[Middleware] ${name || 'anonymous'} completed in ${duration}ms`);
37
+
38
+ return result;
39
+ } catch (error) {
40
+ frameworkLog('error', `[Middleware] Error in ${name || 'anonymous'}: ${error.message}`);
41
+ throw new AfriCodeError(`Middleware error in ${name || 'anonymous'}: ${error.message}`, 'MIDDLEWARE_EXECUTION_ERROR', { cause: error }, 500);
42
+ }
43
+ };
44
+
45
+ // Store metadata in a WeakMap to avoid modifying the function object
46
+ this.middlewareMeta = this.middlewareMeta || new WeakMap();
47
+ this.middlewareMeta.set(wrappedFn, {
48
+ original: fn,
49
+ name: name || fn.name || 'anonymous'
50
+ });
51
+
52
+ this.middlewares.push(wrappedFn);
21
53
  return this;
22
54
  }
23
55
 
56
+ /**
57
+ * Run middleware pipeline
58
+ * @param {Object} context - Request context
59
+ * @returns {Promise<Response|null>}
60
+ */
24
61
  async run(context) {
25
- for (const mw of this.middlewares) {
26
- const result = await mw(context);
62
+ if (this.isRunning) {
63
+ throw new MiddlewareError('Middleware pipeline is already running');
64
+ }
65
+
66
+ this.isRunning = true;
67
+
68
+ try {
69
+ for (const mw of this.middlewares) {
70
+ const result = await mw(context);
27
71
 
28
- if (result instanceof Response) {
29
- return result;
30
- }
72
+ if (result instanceof Response) {
73
+ return result;
74
+ }
31
75
 
32
- // middleware can short-circuit request
33
- if (result?.response) {
34
- return result.response;
35
- }
76
+ // middleware can short-circuit request
77
+ if (result?.response) {
78
+ return result.response;
79
+ }
36
80
 
37
- // allow mutation of context
38
- if (result?.context) {
39
- Object.assign(context, result.context);
81
+ // allow mutation of context
82
+ if (result?.context) {
83
+ Object.assign(context, result.context);
84
+ }
40
85
  }
41
- }
42
86
 
43
- return null;
87
+ return null;
88
+ } finally {
89
+ this.isRunning = false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get middleware count
95
+ * @returns {number}
96
+ */
97
+ count() {
98
+ return this.middlewares.length;
99
+ }
100
+
101
+ /**
102
+ * Clear all middlewares
103
+ */
104
+ clear() {
105
+ this.middlewares = [];
106
+ return this;
44
107
  }
45
108
  }
46
109