@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.
- package/AGENT_INSTRUCTIONS.md +595 -595
- package/COMPONENT_SCHEMA.json +1800 -991
- package/bin/create-africode.js +87 -25
- package/components/auth-form.js +154 -0
- package/components/index.js +10 -0
- package/components/kyc-upload.js +173 -0
- package/components/nav-drawer.js +217 -0
- package/components/transaction-ledger.js +138 -0
- package/components/wallet-balance.js +114 -0
- package/core/a2ui-schema-manager.js +1 -1
- package/core/a2ui.js +178 -1
- package/core/bun-runtime.js +122 -7
- package/core/cli/commands/build.js +30 -5
- package/core/cli/ui.js +13 -3
- package/core/compliance.js +201 -0
- package/core/middleware.js +80 -17
- package/core/patterns.js +168 -0
- package/core/request-analytics.js +254 -0
- package/core/request-identity.js +79 -29
- package/core/sdk.js +4 -1
- package/core/validation.js +13 -0
- package/dist/africode.js +858 -457
- package/dist/africode.js.map +14 -9
- package/dist/build-info.json +3 -3
- package/dist/components.js +784 -383
- package/dist/components.js.map +11 -6
- package/package.json +1 -1
- package/templates/starter/package.json +5 -5
- package/templates/starter/src/index.js +18 -0
- package/templates/starter/src/pages/index.js +1 -1
- package/templates/starter-3d/package.json +5 -5
- package/templates/starter-3d/src/pages/index.js +1 -1
- package/templates/starter-dashboard/.env.example +21 -0
- package/templates/starter-dashboard/africode.config.js +20 -0
- package/templates/starter-dashboard/package.json +14 -0
- package/templates/starter-dashboard/src/index.js +17 -0
- package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
- package/templates/starter-dashboard/src/pages/index.html +118 -0
- package/templates/starter-dashboard/src/pages/index.js +110 -0
- package/templates/starter-dashboard/src/styles/main.css +172 -0
- package/templates/starter-fintech/.env.example +28 -0
- package/templates/starter-fintech/africode.config.js +20 -0
- package/templates/starter-fintech/package.json +15 -0
- package/templates/starter-fintech/src/index.js +17 -0
- package/templates/starter-fintech/src/pages/api/auth.js +45 -0
- package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
- package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
- package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
- package/templates/starter-fintech/src/pages/index.html +169 -0
- package/templates/starter-fintech/src/pages/index.js +161 -0
- package/templates/starter-fintech/src/styles/main.css +246 -0
- package/templates/starter-react/package.json +5 -5
- package/templates/starter-react/src/pages/index.js +1 -1
- package/templates/starter-tailwind/package.json +5 -5
- package/templates/starter-tailwind/src/pages/index.js +1 -1
- package/templates/starter-website/.env.example +18 -0
- package/templates/starter-website/africode.config.js +20 -0
- package/templates/starter-website/package.json +14 -0
- package/templates/starter-website/src/index.js +17 -0
- package/templates/starter-website/src/pages/index.html +124 -0
- package/templates/starter-website/src/pages/index.js +116 -0
- package/templates/starter-website/src/styles/main.css +195 -0
package/core/bun-runtime.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
package/core/compliance.js
CHANGED
|
@@ -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
|
+
|
package/core/middleware.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
25
|
+
throw new AfriCodeError('Middleware must be a function', 'MIDDLEWARE_INVALID', {}, 500);
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
if (result instanceof Response) {
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
31
75
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
76
|
+
// middleware can short-circuit request
|
|
77
|
+
if (result?.response) {
|
|
78
|
+
return result.response;
|
|
79
|
+
}
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
// allow mutation of context
|
|
82
|
+
if (result?.context) {
|
|
83
|
+
Object.assign(context, result.context);
|
|
84
|
+
}
|
|
40
85
|
}
|
|
41
|
-
}
|
|
42
86
|
|
|
43
|
-
|
|
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
|
|