@africode/core 5.0.1 → 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 +103 -69
- 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 +74 -12
- 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 +3 -3
- package/scripts/generate-component-schema.js +1 -1
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,12 +172,17 @@ 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
181
|
|
|
182
|
+
async initiatePayment(request) {
|
|
183
|
+
return this.initiateMerchantPayment(request);
|
|
184
|
+
}
|
|
185
|
+
|
|
177
186
|
/**
|
|
178
187
|
* Step 2: Customer Identification (via Prompt)
|
|
179
188
|
* Customer enters PIN to identify with NIDA
|
|
@@ -226,11 +235,22 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
226
235
|
return {
|
|
227
236
|
ok: true,
|
|
228
237
|
customerId: nidaResult.nin,
|
|
229
|
-
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
|
+
},
|
|
230
245
|
nextStep: 'CONFIRM'
|
|
231
246
|
};
|
|
232
247
|
}
|
|
233
248
|
|
|
249
|
+
async verifyCustomerIdentity(paymentRequestId, customerIdentity) {
|
|
250
|
+
const identity = customerIdentity || {};
|
|
251
|
+
return this.identifyCustomer(paymentRequestId, identity.nin, identity.pin);
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
/**
|
|
235
255
|
* Step 3: AML Screening
|
|
236
256
|
* Check customer against FIU suspicious persons list
|
|
@@ -362,7 +382,7 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
362
382
|
};
|
|
363
383
|
|
|
364
384
|
} catch (error) {
|
|
365
|
-
|
|
385
|
+
frameworkLog('error', '[Lipa Namba] Processing error:', error);
|
|
366
386
|
return {
|
|
367
387
|
ok: false,
|
|
368
388
|
error: error.message,
|
|
@@ -438,12 +458,12 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
438
458
|
async _checkMerchantLimits(merchantId, amount) {
|
|
439
459
|
// Check daily limit
|
|
440
460
|
const dailyTotal = await this._getMerchantDailyTotal(merchantId);
|
|
441
|
-
if (dailyTotal + amount > this.
|
|
461
|
+
if (dailyTotal + amount > this.regulatoryLimits.merchant.daily) {
|
|
442
462
|
return { ok: false, error: 'DAILY_LIMIT_EXCEEDED' };
|
|
443
463
|
}
|
|
444
464
|
|
|
445
465
|
// Check per-transaction limit
|
|
446
|
-
if (amount > this.
|
|
466
|
+
if (amount > this.regulatoryLimits.merchant.perTransaction) {
|
|
447
467
|
return { ok: false, error: 'TRANSACTION_LIMIT_EXCEEDED' };
|
|
448
468
|
}
|
|
449
469
|
|
|
@@ -452,11 +472,11 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
452
472
|
|
|
453
473
|
async _checkCustomerLimits(customerId, amount) {
|
|
454
474
|
const dailyTotal = await this._getCustomerDailyTotal(customerId);
|
|
455
|
-
if (dailyTotal + amount > this.
|
|
475
|
+
if (dailyTotal + amount > this.regulatoryLimits.customer.daily) {
|
|
456
476
|
return { ok: false, error: 'DAILY_LIMIT_EXCEEDED' };
|
|
457
477
|
}
|
|
458
478
|
|
|
459
|
-
if (amount > this.
|
|
479
|
+
if (amount > this.regulatoryLimits.customer.perTransaction) {
|
|
460
480
|
return { ok: false, error: 'TRANSACTION_LIMIT_EXCEEDED' };
|
|
461
481
|
}
|
|
462
482
|
|
|
@@ -467,6 +487,42 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
467
487
|
return `LN_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
468
488
|
}
|
|
469
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
|
+
|
|
470
526
|
async _verifyNIDA(nin, pin) {
|
|
471
527
|
// Implementation calls NIDA CIG
|
|
472
528
|
// For now, return mock
|
|
@@ -480,16 +536,22 @@ export class LipaNambaJourney extends EventEmitter {
|
|
|
480
536
|
}
|
|
481
537
|
|
|
482
538
|
async _storePaymentRequest(request) {
|
|
483
|
-
|
|
539
|
+
this.sessions.set(request.id, { ...request });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async _storePaymentSession(sessionData) {
|
|
543
|
+
this.sessions.set(sessionData.id, { ...sessionData });
|
|
484
544
|
}
|
|
485
545
|
|
|
486
546
|
async _getPaymentRequest(id) {
|
|
487
|
-
|
|
488
|
-
return null;
|
|
547
|
+
return this.sessions.get(id) || null;
|
|
489
548
|
}
|
|
490
549
|
|
|
491
550
|
async _updatePaymentRequest(id, updates) {
|
|
492
|
-
|
|
551
|
+
const current = this.sessions.get(id);
|
|
552
|
+
if (current) {
|
|
553
|
+
this.sessions.set(id, { ...current, ...updates });
|
|
554
|
+
}
|
|
493
555
|
}
|
|
494
556
|
|
|
495
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
|
+
}
|