@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/html.d.ts
CHANGED
|
@@ -1,116 +1,30 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* AfriCode HTML Template Engine - TypeScript Definitions
|
|
3
3
|
* JSX-free template literal engine
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Usage:
|
|
11
|
-
* ```ts
|
|
12
|
-
* const name = 'Alice';
|
|
13
|
-
* const result = html`<h1>Hello ${name}</h1>`;
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* @param strings - Template string parts
|
|
17
|
-
* @param values - Interpolated values
|
|
18
|
-
* @returns Rendered HTML string
|
|
19
|
-
*/
|
|
20
|
-
export function html(
|
|
21
|
-
strings: TemplateStringsArray,
|
|
22
|
-
...values: any[]
|
|
23
|
-
): string;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Render options
|
|
27
|
-
*/
|
|
28
|
-
export interface RenderOptions {
|
|
29
|
-
/** Escape HTML special characters */
|
|
30
|
-
escape?: boolean;
|
|
31
|
-
/** Keep falsy values as string */
|
|
32
|
-
keepFalsy?: boolean;
|
|
33
|
-
/** Custom serializer */
|
|
34
|
-
serializer?: (value: any) => string;
|
|
6
|
+
export class RawHtml {
|
|
7
|
+
constructor(value: string);
|
|
8
|
+
toString(): string;
|
|
9
|
+
valueOf(): string;
|
|
35
10
|
}
|
|
36
11
|
|
|
37
12
|
/**
|
|
38
|
-
*
|
|
13
|
+
* HTML template tag function.
|
|
14
|
+
* Returns a RawHtml wrapper so callers can opt into trusted output.
|
|
39
15
|
*/
|
|
40
|
-
export function
|
|
16
|
+
export function html(
|
|
41
17
|
strings: TemplateStringsArray,
|
|
42
|
-
values: any[]
|
|
43
|
-
|
|
44
|
-
): string;
|
|
18
|
+
...values: any[]
|
|
19
|
+
): RawHtml;
|
|
45
20
|
|
|
46
|
-
/**
|
|
47
|
-
* Layout component props
|
|
48
|
-
*/
|
|
49
21
|
export interface LayoutProps {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/** Vertical alignment */
|
|
55
|
-
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
56
|
-
/** Horizontal justification */
|
|
57
|
-
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
|
|
58
|
-
/** Additional CSS classes */
|
|
59
|
-
class?: string;
|
|
60
|
-
/** Inline styles */
|
|
61
|
-
style?: Record<string, string>;
|
|
22
|
+
title?: string;
|
|
23
|
+
meta?: string;
|
|
24
|
+
stylesheet?: string | false;
|
|
25
|
+
children?: RawHtml | string;
|
|
62
26
|
}
|
|
63
27
|
|
|
64
|
-
|
|
65
|
-
* Layout component class
|
|
66
|
-
*/
|
|
67
|
-
export class Layout {
|
|
68
|
-
constructor(props?: LayoutProps);
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Add child element or text
|
|
72
|
-
*/
|
|
73
|
-
addChild(child: HTMLElement | string): this;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Render to HTML element
|
|
77
|
-
*/
|
|
78
|
-
render(): HTMLElement;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get HTML string representation
|
|
82
|
-
*/
|
|
83
|
-
toString(): string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Flex layout shorthand
|
|
88
|
-
*/
|
|
89
|
-
export function flex(
|
|
90
|
-
options?: LayoutProps
|
|
91
|
-
): HTMLElement;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Grid layout shorthand
|
|
95
|
-
*/
|
|
96
|
-
export function grid(
|
|
97
|
-
columnCount: number,
|
|
98
|
-
gap?: number
|
|
99
|
-
): HTMLElement;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Create safe text node
|
|
103
|
-
*/
|
|
104
|
-
export function text(content: string): string;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Escape HTML entities
|
|
108
|
-
*/
|
|
109
|
-
export function escape(str: string): string;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Unescape HTML entities
|
|
113
|
-
*/
|
|
114
|
-
export function unescape(str: string): string;
|
|
28
|
+
export function Layout(props?: LayoutProps): RawHtml;
|
|
115
29
|
|
|
116
30
|
export default html;
|
package/core/html.js
CHANGED
|
@@ -1,160 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AfriCode HTML Template Engine
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* This is a lightweight, zero-compile template system (JSX alternative).
|
|
6
|
-
*
|
|
3
|
+
* Phase 2: Defined Rendering System
|
|
4
|
+
*
|
|
7
5
|
* @module core/html
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
/**
|
|
11
|
-
* RawHtml wrapper class
|
|
12
|
-
* Marks HTML content as safe and prevents escaping when interpolated
|
|
13
|
-
*
|
|
14
|
-
* @class RawHtml
|
|
15
|
-
* @param {string} content - The raw HTML content
|
|
16
|
-
*/
|
|
17
8
|
export class RawHtml {
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
9
|
+
constructor(value) {
|
|
10
|
+
this.value = value;
|
|
11
|
+
this.__type = 'RawHtml';
|
|
12
|
+
Object.freeze(this);
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
toString() {
|
|
23
|
-
return this.
|
|
16
|
+
return this.value;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
valueOf() {
|
|
27
|
-
return this.
|
|
20
|
+
return this.value;
|
|
28
21
|
}
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const map = {
|
|
41
|
-
'&': '&',
|
|
42
|
-
'<': '<',
|
|
43
|
-
'>': '>',
|
|
44
|
-
'"': '"',
|
|
45
|
-
"'": '''
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return str.replace(/[&<>"']/g, char => map[char]);
|
|
24
|
+
// 🔒 Escape unsafe values
|
|
25
|
+
function escapeHtml(value) {
|
|
26
|
+
return String(value)
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, """)
|
|
31
|
+
.replace(/'/g, "'");
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* - If array: recursively process and join
|
|
55
|
-
* - If null/undefined/false: render empty string
|
|
56
|
-
* - Otherwise: escape as text
|
|
57
|
-
*
|
|
58
|
-
* @param {any} value - Value to interpolate
|
|
59
|
-
* @returns {string}
|
|
60
|
-
*/
|
|
61
|
-
function interpolateValue(value) {
|
|
62
|
-
// RawHtml: insert as raw markup (safe for nested templates)
|
|
63
|
-
if (value instanceof RawHtml) {
|
|
64
|
-
return value.toString();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Arrays: recursively process and join
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
return value.map(v => interpolateValue(v)).join('');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Falsy values: render empty string
|
|
73
|
-
if (value === null || value === undefined || value === false) {
|
|
74
|
-
return '';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Functions: convert to string (for component functions, better to call them first)
|
|
78
|
-
if (typeof value === 'function') {
|
|
79
|
-
return escapeHtml(String(value));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Everything else: escape as text
|
|
83
|
-
return escapeHtml(String(value));
|
|
84
|
-
}
|
|
34
|
+
// 🧠 Main rendering function
|
|
35
|
+
export const html = (strings, ...values) => {
|
|
36
|
+
let result = "";
|
|
85
37
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* Safely handles nested templates, arrays, and text escaping.
|
|
89
|
-
*
|
|
90
|
-
* Returns a RawHtml wrapper so nested templates aren't double-escaped.
|
|
91
|
-
*
|
|
92
|
-
* @param {TemplateStringsArray} strings
|
|
93
|
-
* @param {...any} values
|
|
94
|
-
* @returns {RawHtml}
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* // Nested templates (no escaping of markup)
|
|
98
|
-
* const button = html`<af-button>Click</af-button>`;
|
|
99
|
-
* const page = html`<div>${button}</div>`;
|
|
100
|
-
* // Output: <div><af-button>Click</af-button></div>
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* // Plain text (escaped for safety)
|
|
104
|
-
* const user = html`<p>${userInput}</p>`;
|
|
105
|
-
* // If userInput = "<script>alert('xss')</script>"
|
|
106
|
-
* // Output: <p><script>alert('xss')</script></p>
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* // Arrays of templates
|
|
110
|
-
* const items = [html`<li>Item 1</li>`, html`<li>Item 2</li>`];
|
|
111
|
-
* const list = html`<ul>${items}</ul>`;
|
|
112
|
-
* // Output: <ul><li>Item 1</li><li>Item 2</li></ul>
|
|
113
|
-
*/
|
|
114
|
-
export function html(strings, ...values) {
|
|
115
|
-
let result = '';
|
|
38
|
+
for (let i = 0; i < strings.length; i++) {
|
|
39
|
+
result += strings[i];
|
|
116
40
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
result += interpolateValue(values[i]);
|
|
41
|
+
if (i < values.length) {
|
|
42
|
+
result += interpolateValue(values[i]);
|
|
120
43
|
}
|
|
44
|
+
}
|
|
121
45
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Return as RawHtml so nested html() calls aren't escaped
|
|
125
|
-
return new RawHtml(result);
|
|
126
|
-
}
|
|
46
|
+
return new RawHtml(result);
|
|
47
|
+
};
|
|
127
48
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
* Similar to Next.js layout.js concept
|
|
131
|
-
*
|
|
132
|
-
* @param {Object} options
|
|
133
|
-
* @param {string} options.title - Page title
|
|
134
|
-
* @param {string} options.meta - Meta tags
|
|
135
|
-
* @param {RawHtml|string} options.children - Page content
|
|
136
|
-
* @returns {RawHtml}
|
|
137
|
-
*/
|
|
138
|
-
export function Layout({ title = 'AfriCode App', meta = '', children }) {
|
|
139
|
-
return html`<!DOCTYPE html>
|
|
49
|
+
export function Layout({ title = 'AfriCode App', meta = '', stylesheet = '/styles/africanity.css', children } = {}) {
|
|
50
|
+
return html`<!DOCTYPE html>
|
|
140
51
|
<html lang="en">
|
|
141
52
|
<head>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<script type="module">
|
|
149
|
-
import { init } from '/core/sdk.js';
|
|
150
|
-
init();
|
|
151
|
-
</script>
|
|
53
|
+
<meta charset="UTF-8">
|
|
54
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
55
|
+
<title>${title}</title>
|
|
56
|
+
${meta}
|
|
57
|
+
${stylesheet ? html`<link rel="stylesheet" href="${stylesheet}">` : ''}
|
|
58
|
+
<script type="module" src="/core/sdk.js"></script>
|
|
152
59
|
</head>
|
|
153
60
|
<body>
|
|
154
|
-
|
|
61
|
+
${children}
|
|
155
62
|
</body>
|
|
156
63
|
</html>`;
|
|
157
64
|
}
|
|
158
65
|
|
|
66
|
+
function interpolateValue(value) {
|
|
67
|
+
if (value instanceof RawHtml) {
|
|
68
|
+
return value.toString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return value.map(interpolateValue).join('');
|
|
73
|
+
}
|
|
159
74
|
|
|
160
|
-
|
|
75
|
+
if (value === null || value === undefined || value === false) {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value === true) {
|
|
80
|
+
return 'true';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return escapeHtml(value);
|
|
84
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { EventEmitter } from 'events';
|
|
15
|
+
import { frameworkLog } from './logging.js';
|
|
15
16
|
|
|
16
17
|
export class LipaNambaJourney extends EventEmitter {
|
|
17
18
|
constructor(config = {}) {
|
|
@@ -26,6 +27,8 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
26
27
|
...config
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
this.sessions = new Map();
|
|
31
|
+
|
|
29
32
|
// BoT Regulatory Limits (as of 2026)
|
|
30
33
|
this.regulatoryLimits = {
|
|
31
34
|
merchant: {
|
|
@@ -92,7 +95,8 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
92
95
|
// Step 4: Payment Confirmation
|
|
93
96
|
const confirmation = await this.confirmPayment(
|
|
94
97
|
initiation.paymentRequestId,
|
|
95
|
-
|
|
98
|
+
identification.customerId,
|
|
99
|
+
confirmationData.code
|
|
96
100
|
);
|
|
97
101
|
if (!confirmation.ok) {
|
|
98
102
|
throw new Error(`Confirmation failed: ${confirmation.error}`);
|
|
@@ -120,7 +124,7 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
120
124
|
const schema = z.object({
|
|
121
125
|
merchantId: z.string().min(1),
|
|
122
126
|
customerPhone: z.string().regex(/^255\d{9}$/), // Tanzanian format
|
|
123
|
-
amount: z.number().positive().max(this.regulatoryLimits.merchant.
|
|
127
|
+
amount: z.number().positive().max(this.regulatoryLimits.merchant.daily),
|
|
124
128
|
currency: z.string().default('TZS'),
|
|
125
129
|
description: z.string().min(1).max(100),
|
|
126
130
|
reference: z.string().optional(),
|
|
@@ -168,61 +172,15 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
168
172
|
return {
|
|
169
173
|
ok: true,
|
|
170
174
|
paymentRequestId,
|
|
175
|
+
redirectUrl: `/pay/${paymentRequestId}`,
|
|
171
176
|
sessionData,
|
|
172
177
|
nextStep: 'IDENTIFY_CUSTOMER',
|
|
173
178
|
expiresIn: this.regulatoryLimits.sessionTimeout / 1000
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
|
-
const schema = z.object({
|
|
177
|
-
merchantId: z.string().min(1),
|
|
178
|
-
customerPhone: z.string().regex(/^255\d{9}$/), // Tanzanian format
|
|
179
|
-
amount: z.number().positive(),
|
|
180
|
-
description: z.string().min(1),
|
|
181
|
-
reference: z.string().optional(),
|
|
182
|
-
callbackUrl: z.string().url().optional()
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const validated = schema.parse(request);
|
|
186
181
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
validated.merchantId,
|
|
190
|
-
validated.amount
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
if (!merchantStatus.ok) {
|
|
194
|
-
return {
|
|
195
|
-
ok: false,
|
|
196
|
-
error: merchantStatus.error,
|
|
197
|
-
code: 'MERCHANT_LIMIT_EXCEEDED'
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Generate payment request ID
|
|
202
|
-
const paymentRequestId = this._generatePaymentId();
|
|
203
|
-
|
|
204
|
-
// Store payment request
|
|
205
|
-
const paymentRequest = {
|
|
206
|
-
id: paymentRequestId,
|
|
207
|
-
merchantId: validated.merchantId,
|
|
208
|
-
customerPhone: validated.customerPhone,
|
|
209
|
-
amount: validated.amount,
|
|
210
|
-
description: validated.description,
|
|
211
|
-
reference: validated.reference || paymentRequestId,
|
|
212
|
-
status: 'INITIATED',
|
|
213
|
-
createdAt: new Date(),
|
|
214
|
-
expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 min expiry
|
|
215
|
-
callbackUrl: validated.callbackUrl
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
await this._storePaymentRequest(paymentRequest);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
ok: true,
|
|
222
|
-
paymentRequestId,
|
|
223
|
-
redirectUrl: `https://lipa.example.com/pay/${paymentRequestId}`,
|
|
224
|
-
expiresIn: 30 * 60 // seconds
|
|
225
|
-
};
|
|
182
|
+
async initiatePayment(request) {
|
|
183
|
+
return this.initiateMerchantPayment(request);
|
|
226
184
|
}
|
|
227
185
|
|
|
228
186
|
/**
|
|
@@ -277,11 +235,22 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
277
235
|
return {
|
|
278
236
|
ok: true,
|
|
279
237
|
customerId: nidaResult.nin,
|
|
280
|
-
customerName: nidaResult.
|
|
238
|
+
customerName: `${nidaResult.firstName} ${nidaResult.lastName}`,
|
|
239
|
+
customerData: {
|
|
240
|
+
nin: nidaResult.nin,
|
|
241
|
+
firstName: nidaResult.firstName,
|
|
242
|
+
lastName: nidaResult.lastName,
|
|
243
|
+
customerName: `${nidaResult.firstName} ${nidaResult.lastName}`
|
|
244
|
+
},
|
|
281
245
|
nextStep: 'CONFIRM'
|
|
282
246
|
};
|
|
283
247
|
}
|
|
284
248
|
|
|
249
|
+
async verifyCustomerIdentity(paymentRequestId, customerIdentity) {
|
|
250
|
+
const identity = customerIdentity || {};
|
|
251
|
+
return this.identifyCustomer(paymentRequestId, identity.nin, identity.pin);
|
|
252
|
+
}
|
|
253
|
+
|
|
285
254
|
/**
|
|
286
255
|
* Step 3: AML Screening
|
|
287
256
|
* Check customer against FIU suspicious persons list
|
|
@@ -413,7 +382,7 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
413
382
|
};
|
|
414
383
|
|
|
415
384
|
} catch (error) {
|
|
416
|
-
|
|
385
|
+
frameworkLog('error', '[Lipa Namba] Processing error:', error);
|
|
417
386
|
return {
|
|
418
387
|
ok: false,
|
|
419
388
|
error: error.message,
|
|
@@ -489,12 +458,12 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
489
458
|
async _checkMerchantLimits(merchantId, amount) {
|
|
490
459
|
// Check daily limit
|
|
491
460
|
const dailyTotal = await this._getMerchantDailyTotal(merchantId);
|
|
492
|
-
if (dailyTotal + amount > this.
|
|
461
|
+
if (dailyTotal + amount > this.regulatoryLimits.merchant.daily) {
|
|
493
462
|
return { ok: false, error: 'DAILY_LIMIT_EXCEEDED' };
|
|
494
463
|
}
|
|
495
464
|
|
|
496
465
|
// Check per-transaction limit
|
|
497
|
-
if (amount > this.
|
|
466
|
+
if (amount > this.regulatoryLimits.merchant.perTransaction) {
|
|
498
467
|
return { ok: false, error: 'TRANSACTION_LIMIT_EXCEEDED' };
|
|
499
468
|
}
|
|
500
469
|
|
|
@@ -503,11 +472,11 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
503
472
|
|
|
504
473
|
async _checkCustomerLimits(customerId, amount) {
|
|
505
474
|
const dailyTotal = await this._getCustomerDailyTotal(customerId);
|
|
506
|
-
if (dailyTotal + amount > this.
|
|
475
|
+
if (dailyTotal + amount > this.regulatoryLimits.customer.daily) {
|
|
507
476
|
return { ok: false, error: 'DAILY_LIMIT_EXCEEDED' };
|
|
508
477
|
}
|
|
509
478
|
|
|
510
|
-
if (amount > this.
|
|
479
|
+
if (amount > this.regulatoryLimits.customer.perTransaction) {
|
|
511
480
|
return { ok: false, error: 'TRANSACTION_LIMIT_EXCEEDED' };
|
|
512
481
|
}
|
|
513
482
|
|
|
@@ -518,6 +487,42 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
518
487
|
return `LN_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
519
488
|
}
|
|
520
489
|
|
|
490
|
+
_generateSecurePaymentId() {
|
|
491
|
+
return this._generatePaymentId();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async _verifyMerchantCompliance(merchantId) {
|
|
495
|
+
if (!merchantId) {
|
|
496
|
+
return { ok: false, error: 'MERCHANT_NOT_FOUND' };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return { ok: true };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
_handlePaymentInitiated(sessionData) {
|
|
503
|
+
return sessionData;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_handleCustomerIdentified(data) {
|
|
507
|
+
return data;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_handleAMLScreened(data) {
|
|
511
|
+
return data;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
_handlePaymentConfirmed(data) {
|
|
515
|
+
return data;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_handlePaymentCompleted(data) {
|
|
519
|
+
return data;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
_handlePaymentFailed(data) {
|
|
523
|
+
return data;
|
|
524
|
+
}
|
|
525
|
+
|
|
521
526
|
async _verifyNIDA(nin, pin) {
|
|
522
527
|
// Implementation calls NIDA CIG
|
|
523
528
|
// For now, return mock
|
|
@@ -531,16 +536,22 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
531
536
|
}
|
|
532
537
|
|
|
533
538
|
async _storePaymentRequest(request) {
|
|
534
|
-
|
|
539
|
+
this.sessions.set(request.id, { ...request });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async _storePaymentSession(sessionData) {
|
|
543
|
+
this.sessions.set(sessionData.id, { ...sessionData });
|
|
535
544
|
}
|
|
536
545
|
|
|
537
546
|
async _getPaymentRequest(id) {
|
|
538
|
-
|
|
539
|
-
return null;
|
|
547
|
+
return this.sessions.get(id) || null;
|
|
540
548
|
}
|
|
541
549
|
|
|
542
550
|
async _updatePaymentRequest(id, updates) {
|
|
543
|
-
|
|
551
|
+
const current = this.sessions.get(id);
|
|
552
|
+
if (current) {
|
|
553
|
+
this.sessions.set(id, { ...current, ...updates });
|
|
554
|
+
}
|
|
544
555
|
}
|
|
545
556
|
|
|
546
557
|
async _getMerchantDailyTotal(merchantId) {
|
package/core/logging.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const isTestEnvironment = () => Boolean(globalThis.__AFRICODE_TEST__ || globalThis.__AFRICODE_SILENT_LOGS__);
|
|
2
|
+
|
|
3
|
+
export function frameworkLog(level, ...args) {
|
|
4
|
+
if (isTestEnvironment()) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const logger = console[level] || console.log;
|
|
9
|
+
logger.call(console, ...args);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isFrameworkLoggingMuted() {
|
|
13
|
+
return isTestEnvironment();
|
|
14
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Middleware System
|
|
3
|
+
* Core request interception pipeline
|
|
4
|
+
*
|
|
5
|
+
* @module core/middleware
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { frameworkLog } from './logging.js';
|
|
9
|
+
|
|
10
|
+
export class MiddlewareManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.middlewares = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
use(fn) {
|
|
16
|
+
if (typeof fn !== 'function') {
|
|
17
|
+
throw new Error('Middleware must be a function');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.middlewares.push(fn);
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async run(context) {
|
|
25
|
+
for (const mw of this.middlewares) {
|
|
26
|
+
const result = await mw(context);
|
|
27
|
+
|
|
28
|
+
if (result instanceof Response) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// middleware can short-circuit request
|
|
33
|
+
if (result?.response) {
|
|
34
|
+
return result.response;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// allow mutation of context
|
|
38
|
+
if (result?.context) {
|
|
39
|
+
Object.assign(context, result.context);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Logging middleware - logs all requests
|
|
49
|
+
*/
|
|
50
|
+
export function loggerMiddleware() {
|
|
51
|
+
return async (ctx) => {
|
|
52
|
+
frameworkLog('log', `[AfriCode] ${ctx.req.method} ${ctx.pathname}`);
|
|
53
|
+
return {};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Opt-in auth guard middleware for protected routes.
|
|
59
|
+
*/
|
|
60
|
+
export function authMiddleware(options = {}) {
|
|
61
|
+
return async (ctx) => {
|
|
62
|
+
const {
|
|
63
|
+
protectedPaths = [],
|
|
64
|
+
authorize = () => false,
|
|
65
|
+
unauthorizedResponse = () => new Response('Unauthorized', { status: 401 })
|
|
66
|
+
} = options;
|
|
67
|
+
|
|
68
|
+
const isProtected = protectedPaths.some((path) => ctx.pathname.startsWith(path));
|
|
69
|
+
|
|
70
|
+
if (isProtected) {
|
|
71
|
+
const isAllowed = await authorize(ctx);
|
|
72
|
+
|
|
73
|
+
if (!isAllowed) {
|
|
74
|
+
return {
|
|
75
|
+
response: unauthorizedResponse(ctx)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
};
|
|
82
|
+
}
|