@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/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
- * HTML template tag function
8
- * Safely renders template literals as HTML strings
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
- * Safe HTML rendering with custom options
13
+ * HTML template tag function.
14
+ * Returns a RawHtml wrapper so callers can opt into trusted output.
39
15
  */
40
- export function renderHtml(
16
+ export function html(
41
17
  strings: TemplateStringsArray,
42
- values: any[],
43
- options?: RenderOptions
44
- ): string;
18
+ ...values: any[]
19
+ ): RawHtml;
45
20
 
46
- /**
47
- * Layout component props
48
- */
49
21
  export interface LayoutProps {
50
- /** Use vertical layout (default: horizontal) */
51
- vertical?: boolean;
52
- /** Spacing between items (px) */
53
- gap?: number;
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
- * Provides the `html` tagged template literal for writing markup in JavaScript.
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(content) {
19
- this.content = content;
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.content;
16
+ return this.value;
24
17
  }
25
18
 
26
19
  valueOf() {
27
- return this.content;
20
+ return this.value;
28
21
  }
29
22
  }
30
23
 
31
- /**
32
- * Escape HTML special characters to prevent XSS
33
- *
34
- * @param {any} str - Value to escape
35
- * @returns {string} Escaped string
36
- */
37
- function escapeHtml(str) {
38
- if (typeof str !== 'string') {return str;}
39
-
40
- const map = {
41
- '&': '&amp;',
42
- '<': '&lt;',
43
- '>': '&gt;',
44
- '"': '&quot;',
45
- "'": '&#039;'
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, "&amp;")
28
+ .replace(/</g, "&lt;")
29
+ .replace(/>/g, "&gt;")
30
+ .replace(/"/g, "&quot;")
31
+ .replace(/'/g, "&#039;");
49
32
  }
50
33
 
51
- /**
52
- * Safely interpolate a value into HTML
53
- * - If RawHtml: insert as raw markup
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
- * Tagged template literal for HTML
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>&lt;script&gt;alert('xss')&lt;/script&gt;</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
- for (let i = 0; i < values.length; i++) {
118
- result += strings[i];
119
- result += interpolateValue(values[i]);
41
+ if (i < values.length) {
42
+ result += interpolateValue(values[i]);
120
43
  }
44
+ }
121
45
 
122
- result += strings[strings.length - 1];
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
- * Standard Root Layout
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
- <meta charset="UTF-8">
143
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
- <title>${title}</title>
145
- ${meta}
146
- <link rel="stylesheet" href="/styles/africanity.css">
147
- <script type="module" src="/core/sdk.js"></script>
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
- ${children}
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
- export default { html, Layout, RawHtml };
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
- confirmationData
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.perTransaction),
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
- // Check merchant limits
188
- const merchantStatus = await this._checkMerchantLimits(
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.customerName,
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
- console.error('[Lipa Namba] Processing error:', error);
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.merchantLimits.daily) {
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.merchantLimits.perTransaction) {
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.customerLimits.daily) {
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.customerLimits.perTransaction) {
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
- // Store in database
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
- // Retrieve from database
539
- return null;
547
+ return this.sessions.get(id) || null;
540
548
  }
541
549
 
542
550
  async _updatePaymentRequest(id, updates) {
543
- // Update in database
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) {
@@ -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
+ }