@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.
Files changed (62) hide show
  1. package/AGENT_INSTRUCTIONS.md +595 -595
  2. package/COMPONENT_SCHEMA.json +1800 -991
  3. package/bin/create-africode.js +87 -25
  4. package/components/auth-form.js +154 -0
  5. package/components/index.js +10 -0
  6. package/components/kyc-upload.js +173 -0
  7. package/components/nav-drawer.js +217 -0
  8. package/components/transaction-ledger.js +138 -0
  9. package/components/wallet-balance.js +114 -0
  10. package/core/a2ui-schema-manager.js +1 -1
  11. package/core/a2ui.js +178 -1
  12. package/core/bun-runtime.js +122 -7
  13. package/core/cli/commands/build.js +30 -5
  14. package/core/cli/ui.js +13 -3
  15. package/core/compliance.js +201 -0
  16. package/core/middleware.js +80 -17
  17. package/core/patterns.js +168 -0
  18. package/core/request-analytics.js +254 -0
  19. package/core/request-identity.js +79 -29
  20. package/core/sdk.js +4 -1
  21. package/core/validation.js +13 -0
  22. package/dist/africode.js +858 -457
  23. package/dist/africode.js.map +14 -9
  24. package/dist/build-info.json +3 -3
  25. package/dist/components.js +784 -383
  26. package/dist/components.js.map +11 -6
  27. package/package.json +1 -1
  28. package/templates/starter/package.json +5 -5
  29. package/templates/starter/src/index.js +18 -0
  30. package/templates/starter/src/pages/index.js +1 -1
  31. package/templates/starter-3d/package.json +5 -5
  32. package/templates/starter-3d/src/pages/index.js +1 -1
  33. package/templates/starter-dashboard/.env.example +21 -0
  34. package/templates/starter-dashboard/africode.config.js +20 -0
  35. package/templates/starter-dashboard/package.json +14 -0
  36. package/templates/starter-dashboard/src/index.js +17 -0
  37. package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
  38. package/templates/starter-dashboard/src/pages/index.html +118 -0
  39. package/templates/starter-dashboard/src/pages/index.js +110 -0
  40. package/templates/starter-dashboard/src/styles/main.css +172 -0
  41. package/templates/starter-fintech/.env.example +28 -0
  42. package/templates/starter-fintech/africode.config.js +20 -0
  43. package/templates/starter-fintech/package.json +15 -0
  44. package/templates/starter-fintech/src/index.js +17 -0
  45. package/templates/starter-fintech/src/pages/api/auth.js +45 -0
  46. package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
  47. package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
  48. package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
  49. package/templates/starter-fintech/src/pages/index.html +169 -0
  50. package/templates/starter-fintech/src/pages/index.js +161 -0
  51. package/templates/starter-fintech/src/styles/main.css +246 -0
  52. package/templates/starter-react/package.json +5 -5
  53. package/templates/starter-react/src/pages/index.js +1 -1
  54. package/templates/starter-tailwind/package.json +5 -5
  55. package/templates/starter-tailwind/src/pages/index.js +1 -1
  56. package/templates/starter-website/.env.example +18 -0
  57. package/templates/starter-website/africode.config.js +20 -0
  58. package/templates/starter-website/package.json +14 -0
  59. package/templates/starter-website/src/index.js +17 -0
  60. package/templates/starter-website/src/pages/index.html +124 -0
  61. package/templates/starter-website/src/pages/index.js +116 -0
  62. package/templates/starter-website/src/styles/main.css +195 -0
@@ -0,0 +1,45 @@
1
+ function json(data, status = 200) {
2
+ return new Response(JSON.stringify(data), {
3
+ status,
4
+ headers: { 'Content-Type': 'application/json' }
5
+ });
6
+ }
7
+
8
+ function isEmail(value) {
9
+ return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
10
+ }
11
+
12
+ export async function POST(request) {
13
+ const body = await request.json().catch(() => ({}));
14
+
15
+ if (!isEmail(body.email) || typeof body.password !== 'string' || body.password.length < 8) {
16
+ return json({
17
+ ok: false,
18
+ code: 'INVALID_CREDENTIALS',
19
+ error: 'Provide a valid email and a password with at least 8 characters.'
20
+ }, 400);
21
+ }
22
+
23
+ return json({
24
+ ok: true,
25
+ user: {
26
+ id: 'usr_demo_001',
27
+ email: body.email,
28
+ role: 'customer',
29
+ kycStatus: 'verified'
30
+ },
31
+ session: {
32
+ type: 'httpOnly-cookie',
33
+ sameSite: 'Strict',
34
+ expiresIn: 3600
35
+ }
36
+ });
37
+ }
38
+
39
+ export function GET() {
40
+ return json({
41
+ ok: true,
42
+ authenticated: false,
43
+ session: null
44
+ });
45
+ }
@@ -0,0 +1,65 @@
1
+ function json(data, status = 200) {
2
+ return new Response(JSON.stringify(data), {
3
+ status,
4
+ headers: { 'Content-Type': 'application/json' }
5
+ });
6
+ }
7
+
8
+ function normalizeAmount(value) {
9
+ const amount = Number(value);
10
+ return Number.isFinite(amount) ? amount : 0;
11
+ }
12
+
13
+ function validateTransfer(body) {
14
+ const amount = normalizeAmount(body.amount);
15
+
16
+ if (!body.recipient || typeof body.recipient !== 'string') {
17
+ return { ok: false, code: 'RECIPIENT_REQUIRED', error: 'Recipient is required.' };
18
+ }
19
+
20
+ if (amount <= 0) {
21
+ return { ok: false, code: 'AMOUNT_REQUIRED', error: 'Transfer amount must be greater than zero.' };
22
+ }
23
+
24
+ return { ok: true, amount };
25
+ }
26
+
27
+ export async function POST(request) {
28
+ const body = await request.json().catch(() => ({}));
29
+ const validation = validateTransfer(body);
30
+
31
+ if (!validation.ok) {
32
+ return json(validation, 400);
33
+ }
34
+
35
+ const dailyLimit = Number(process.env.AML_DAILY_LIMIT_TZS || 50000000);
36
+
37
+ if (validation.amount > dailyLimit) {
38
+ return json({
39
+ ok: false,
40
+ code: 'AML_LIMIT_EXCEEDED',
41
+ error: 'Transfer requires enhanced review before processing.',
42
+ compliance: {
43
+ checked: true,
44
+ dailyLimit,
45
+ decision: 'blocked'
46
+ }
47
+ }, 403);
48
+ }
49
+
50
+ return json({
51
+ ok: true,
52
+ transfer: {
53
+ id: `trf_${Date.now()}`,
54
+ recipient: body.recipient,
55
+ amount: validation.amount,
56
+ currency: body.currency || 'TZS',
57
+ status: 'pending_settlement'
58
+ },
59
+ compliance: {
60
+ checked: true,
61
+ decision: 'approved',
62
+ provider: process.env.PAYMENT_PROVIDER || 'sandbox'
63
+ }
64
+ }, 202);
65
+ }
@@ -0,0 +1,39 @@
1
+ function json(data, status = 200) {
2
+ return new Response(JSON.stringify(data), {
3
+ status,
4
+ headers: { 'Content-Type': 'application/json' }
5
+ });
6
+ }
7
+
8
+ export function GET() {
9
+ return json({
10
+ ok: true,
11
+ wallet: {
12
+ id: 'wal_demo_tzs',
13
+ currency: 'TZS',
14
+ availableBalance: 1250000,
15
+ ledgerBalance: 1285000,
16
+ dailyLimit: Number(process.env.AML_DAILY_LIMIT_TZS || 50000000)
17
+ },
18
+ transactions: [
19
+ {
20
+ id: 'txn_001',
21
+ type: 'credit',
22
+ amount: 500000,
23
+ currency: 'TZS',
24
+ status: 'settled',
25
+ counterparty: 'Merchant payout',
26
+ createdAt: '2026-07-07T08:00:00.000Z'
27
+ },
28
+ {
29
+ id: 'txn_002',
30
+ type: 'debit',
31
+ amount: 35000,
32
+ currency: 'TZS',
33
+ status: 'screened',
34
+ counterparty: 'Utility payment',
35
+ createdAt: '2026-07-07T09:15:00.000Z'
36
+ }
37
+ ]
38
+ });
39
+ }
@@ -0,0 +1,32 @@
1
+ function json(data, status = 200) {
2
+ return new Response(JSON.stringify(data), {
3
+ status,
4
+ headers: { 'Content-Type': 'application/json' }
5
+ });
6
+ }
7
+
8
+ function getSignature(request) {
9
+ return request.headers.get('x-africode-signature') || request.headers.get('x-webhook-signature');
10
+ }
11
+
12
+ export async function POST(request) {
13
+ const signature = getSignature(request);
14
+ const expected = process.env.WEBHOOK_SIGNING_SECRET || 'whsec_mock_africode_fintech';
15
+
16
+ if (signature !== expected) {
17
+ return json({
18
+ ok: false,
19
+ code: 'INVALID_SIGNATURE',
20
+ error: 'Webhook signature could not be verified.'
21
+ }, 401);
22
+ }
23
+
24
+ const event = await request.json().catch(() => ({}));
25
+
26
+ return json({
27
+ ok: true,
28
+ received: true,
29
+ eventType: event.type || 'payment.sandbox',
30
+ transactionId: event.transactionId || null
31
+ });
32
+ }
@@ -0,0 +1,169 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AfriCode Fintech Starter</title>
7
+ <script type="importmap">
8
+ {
9
+ "imports": {
10
+ "africode": "/node_modules/@africode/core/core/sdk-browser.js",
11
+ "africode/components": "/node_modules/@africode/core/components/index.js"
12
+ }
13
+ }
14
+ </script>
15
+ <link rel="stylesheet" href="/node_modules/@africode/core/styles/africanity.css" />
16
+ <link rel="stylesheet" href="/styles/main.css" />
17
+ <script type="module" src="/node_modules/@africode/core/core/sdk-browser.js"></script>
18
+ </head>
19
+ <body>
20
+ <main class="app-shell">
21
+ <section class="topbar">
22
+ <div class="brand">
23
+ <span class="eyebrow">AfriCode Fintech Starter</span>
24
+ <h1>Secure wallet operations</h1>
25
+ </div>
26
+ <div class="actions">
27
+ <button class="button" id="theme-toggle" type="button">Toggle theme</button>
28
+ <button class="button primary" id="auth-demo" type="button">Run auth check</button>
29
+ </div>
30
+ </section>
31
+
32
+ <section class="grid">
33
+ <div>
34
+ <div class="metrics">
35
+ <article class="metric">
36
+ <span class="label">Available balance</span>
37
+ <strong class="value" id="available-balance">Loading</strong>
38
+ <span class="status good" id="wallet-status">Fetching wallet</span>
39
+ </article>
40
+ <article class="metric">
41
+ <span class="label">Ledger balance</span>
42
+ <strong class="value" id="ledger-balance">Loading</strong>
43
+ <span class="status">Sandbox ledger</span>
44
+ </article>
45
+ <article class="metric">
46
+ <span class="label">Daily limit</span>
47
+ <strong class="value" id="daily-limit">Loading</strong>
48
+ <span class="status warn">AML guarded</span>
49
+ </article>
50
+ </div>
51
+
52
+ <article class="ledger">
53
+ <h2>Transaction ledger</h2>
54
+ <div class="ledger-list" id="ledger-list"></div>
55
+ </article>
56
+ </div>
57
+
58
+ <aside class="form-panel">
59
+ <section class="panel">
60
+ <h2>Transfer simulation</h2>
61
+ <form class="form-grid" id="transfer-form">
62
+ <label>
63
+ Recipient
64
+ <input name="recipient" value="merchant_demo" autocomplete="off" />
65
+ </label>
66
+ <label>
67
+ Amount
68
+ <input name="amount" type="number" min="1" value="25000" />
69
+ </label>
70
+ <button class="button primary" type="submit">Submit transfer</button>
71
+ </form>
72
+ </section>
73
+
74
+ <section class="panel">
75
+ <h2>Webhook verification</h2>
76
+ <button class="button" id="webhook-demo" type="button">Simulate payment webhook</button>
77
+ </section>
78
+
79
+ <div class="notice" id="notice">Ready to call the scaffolded API routes.</div>
80
+ </aside>
81
+ </section>
82
+ </main>
83
+
84
+ <script type="module">
85
+ const formatTZS = (value) => 'TZS ' + Number(value || 0).toLocaleString('en-US');
86
+ const notice = document.querySelector('#notice');
87
+
88
+ function setNotice(message, type = '') {
89
+ notice.className = type ? 'notice ' + type : 'notice';
90
+ notice.textContent = message;
91
+ }
92
+
93
+ async function loadWallet() {
94
+ const response = await fetch('/api/wallet');
95
+ const data = await response.json();
96
+
97
+ document.querySelector('#available-balance').textContent = formatTZS(data.wallet.availableBalance);
98
+ document.querySelector('#ledger-balance').textContent = formatTZS(data.wallet.ledgerBalance);
99
+ document.querySelector('#daily-limit').textContent = formatTZS(data.wallet.dailyLimit);
100
+ document.querySelector('#wallet-status').textContent = 'Wallet ready';
101
+ const ledgerList = document.querySelector('#ledger-list');
102
+ ledgerList.replaceChildren(...data.transactions.map((tx) => {
103
+ const row = document.createElement('div');
104
+ const details = document.createElement('div');
105
+ const counterparty = document.createElement('strong');
106
+ const status = document.createElement('div');
107
+ const amount = document.createElement('span');
108
+
109
+ row.className = 'ledger-row';
110
+ status.className = 'status';
111
+ amount.className = 'amount';
112
+ counterparty.textContent = tx.counterparty;
113
+ status.textContent = tx.status;
114
+ amount.textContent = formatTZS(tx.amount);
115
+
116
+ details.append(counterparty, status);
117
+ row.append(details, amount);
118
+ return row;
119
+ }));
120
+ }
121
+
122
+ document.querySelector('#theme-toggle').addEventListener('click', () => {
123
+ const root = document.documentElement;
124
+ root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
125
+ });
126
+
127
+ document.querySelector('#auth-demo').addEventListener('click', async () => {
128
+ const response = await fetch('/api/auth', {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({ email: 'demo@africode.dev', password: 'sandbox-pass' })
132
+ });
133
+ const data = await response.json();
134
+ setNotice(data.ok ? 'Authenticated demo user with ' + data.user.kycStatus + ' KYC.' : data.error, data.ok ? 'success' : 'error');
135
+ });
136
+
137
+ document.querySelector('#transfer-form').addEventListener('submit', async (event) => {
138
+ event.preventDefault();
139
+ const form = new FormData(event.currentTarget);
140
+ const response = await fetch('/api/transfer', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({
144
+ recipient: form.get('recipient'),
145
+ amount: Number(form.get('amount')),
146
+ currency: 'TZS'
147
+ })
148
+ });
149
+ const data = await response.json();
150
+ setNotice(data.ok ? 'Transfer ' + data.transfer.id + ' is ' + data.transfer.status + '.' : data.error, data.ok ? 'success' : 'error');
151
+ });
152
+
153
+ document.querySelector('#webhook-demo').addEventListener('click', async () => {
154
+ const response = await fetch('/api/webhooks/payment', {
155
+ method: 'POST',
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ 'x-africode-signature': 'whsec_mock_africode_fintech'
159
+ },
160
+ body: JSON.stringify({ type: 'payment.succeeded', transactionId: 'txn_demo' })
161
+ });
162
+ const data = await response.json();
163
+ setNotice(data.ok ? 'Webhook accepted for ' + data.transactionId + '.' : data.error, data.ok ? 'success' : 'error');
164
+ });
165
+
166
+ loadWallet().catch(() => setNotice('Wallet API is unavailable.', 'error'));
167
+ </script>
168
+ </body>
169
+ </html>
@@ -0,0 +1,161 @@
1
+ import { html, Layout } from '@africode/core';
2
+
3
+ export function loader() {
4
+ return { title: 'AfriCode Fintech Starter' };
5
+ }
6
+
7
+ export default function HomePage({ data }) {
8
+ return Layout({
9
+ title: data?.title || 'AfriCode Fintech Starter',
10
+ children: html`
11
+ <main class="app-shell">
12
+ <section class="topbar">
13
+ <div class="brand">
14
+ <span class="eyebrow">AfriCode Fintech Starter</span>
15
+ <h1>Secure wallet operations</h1>
16
+ </div>
17
+ <div class="actions">
18
+ <button class="button" id="theme-toggle" type="button">Toggle theme</button>
19
+ <button class="button primary" id="auth-demo" type="button">Run auth check</button>
20
+ </div>
21
+ </section>
22
+
23
+ <section class="grid">
24
+ <div>
25
+ <div class="metrics">
26
+ <article class="metric">
27
+ <span class="label">Available balance</span>
28
+ <strong class="value" id="available-balance">Loading</strong>
29
+ <span class="status good" id="wallet-status">Fetching wallet</span>
30
+ </article>
31
+ <article class="metric">
32
+ <span class="label">Ledger balance</span>
33
+ <strong class="value" id="ledger-balance">Loading</strong>
34
+ <span class="status">Sandbox ledger</span>
35
+ </article>
36
+ <article class="metric">
37
+ <span class="label">Daily limit</span>
38
+ <strong class="value" id="daily-limit">Loading</strong>
39
+ <span class="status warn">AML guarded</span>
40
+ </article>
41
+ </div>
42
+
43
+ <article class="ledger">
44
+ <h2>Transaction ledger</h2>
45
+ <div class="ledger-list" id="ledger-list"></div>
46
+ </article>
47
+ </div>
48
+
49
+ <aside class="form-panel">
50
+ <section class="panel">
51
+ <h2>Transfer simulation</h2>
52
+ <form class="form-grid" id="transfer-form">
53
+ <label>
54
+ Recipient
55
+ <input name="recipient" value="merchant_demo" autocomplete="off" />
56
+ </label>
57
+ <label>
58
+ Amount
59
+ <input name="amount" type="number" min="1" value="25000" />
60
+ </label>
61
+ <button class="button primary" type="submit">Submit transfer</button>
62
+ </form>
63
+ </section>
64
+
65
+ <section class="panel">
66
+ <h2>Webhook verification</h2>
67
+ <button class="button" id="webhook-demo" type="button">Simulate payment webhook</button>
68
+ </section>
69
+
70
+ <div class="notice" id="notice">Ready to call the scaffolded API routes.</div>
71
+ </aside>
72
+ </section>
73
+ </main>
74
+
75
+ <script type="module">
76
+ const formatTZS = (value) => 'TZS ' + Number(value || 0).toLocaleString('en-US');
77
+ const notice = document.querySelector('#notice');
78
+
79
+ function setNotice(message, type = '') {
80
+ notice.className = type ? 'notice ' + type : 'notice';
81
+ notice.textContent = message;
82
+ }
83
+
84
+ async function loadWallet() {
85
+ const response = await fetch('/api/wallet');
86
+ const data = await response.json();
87
+
88
+ document.querySelector('#available-balance').textContent = formatTZS(data.wallet.availableBalance);
89
+ document.querySelector('#ledger-balance').textContent = formatTZS(data.wallet.ledgerBalance);
90
+ document.querySelector('#daily-limit').textContent = formatTZS(data.wallet.dailyLimit);
91
+ document.querySelector('#wallet-status').textContent = 'Wallet ready';
92
+ const ledgerList = document.querySelector('#ledger-list');
93
+ ledgerList.replaceChildren(...data.transactions.map((tx) => {
94
+ const row = document.createElement('div');
95
+ const details = document.createElement('div');
96
+ const counterparty = document.createElement('strong');
97
+ const status = document.createElement('div');
98
+ const amount = document.createElement('span');
99
+
100
+ row.className = 'ledger-row';
101
+ status.className = 'status';
102
+ amount.className = 'amount';
103
+ counterparty.textContent = tx.counterparty;
104
+ status.textContent = tx.status;
105
+ amount.textContent = formatTZS(tx.amount);
106
+
107
+ details.append(counterparty, status);
108
+ row.append(details, amount);
109
+ return row;
110
+ }));
111
+ }
112
+
113
+ document.querySelector('#theme-toggle').addEventListener('click', () => {
114
+ const root = document.documentElement;
115
+ root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
116
+ });
117
+
118
+ document.querySelector('#auth-demo').addEventListener('click', async () => {
119
+ const response = await fetch('/api/auth', {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({ email: 'demo@africode.dev', password: 'sandbox-pass' })
123
+ });
124
+ const data = await response.json();
125
+ setNotice(data.ok ? 'Authenticated demo user with ' + data.user.kycStatus + ' KYC.' : data.error, data.ok ? 'success' : 'error');
126
+ });
127
+
128
+ document.querySelector('#transfer-form').addEventListener('submit', async (event) => {
129
+ event.preventDefault();
130
+ const form = new FormData(event.currentTarget);
131
+ const response = await fetch('/api/transfer', {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify({
135
+ recipient: form.get('recipient'),
136
+ amount: Number(form.get('amount')),
137
+ currency: 'TZS'
138
+ })
139
+ });
140
+ const data = await response.json();
141
+ setNotice(data.ok ? 'Transfer ' + data.transfer.id + ' is ' + data.transfer.status + '.' : data.error, data.ok ? 'success' : 'error');
142
+ });
143
+
144
+ document.querySelector('#webhook-demo').addEventListener('click', async () => {
145
+ const response = await fetch('/api/webhooks/payment', {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ 'x-africode-signature': 'whsec_mock_africode_fintech'
150
+ },
151
+ body: JSON.stringify({ type: 'payment.succeeded', transactionId: 'txn_demo' })
152
+ });
153
+ const data = await response.json();
154
+ setNotice(data.ok ? 'Webhook accepted for ' + data.transactionId + '.' : data.error, data.ok ? 'success' : 'error');
155
+ });
156
+
157
+ loadWallet().catch(() => setNotice('Wallet API is unavailable.', 'error'));
158
+ </script>
159
+ `
160
+ });
161
+ }