@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/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,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.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
+ },
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
- console.error('[Lipa Namba] Processing error:', error);
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.merchantLimits.daily) {
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.merchantLimits.perTransaction) {
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.customerLimits.daily) {
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.customerLimits.perTransaction) {
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
- // Store in database
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
- // Retrieve from database
488
- return null;
547
+ return this.sessions.get(id) || null;
489
548
  }
490
549
 
491
550
  async _updatePaymentRequest(id, updates) {
492
- // Update in database
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) {
@@ -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
+ }