@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.
- package/AGENT_INSTRUCTIONS.md +595 -595
- package/COMPONENT_SCHEMA.json +1800 -991
- package/bin/create-africode.js +87 -25
- package/components/auth-form.js +154 -0
- package/components/index.js +10 -0
- package/components/kyc-upload.js +173 -0
- package/components/nav-drawer.js +217 -0
- package/components/transaction-ledger.js +138 -0
- package/components/wallet-balance.js +114 -0
- package/core/a2ui-schema-manager.js +1 -1
- package/core/a2ui.js +178 -1
- package/core/bun-runtime.js +122 -7
- package/core/cli/commands/build.js +30 -5
- package/core/cli/ui.js +13 -3
- package/core/compliance.js +201 -0
- package/core/middleware.js +80 -17
- package/core/patterns.js +168 -0
- package/core/request-analytics.js +254 -0
- package/core/request-identity.js +79 -29
- package/core/sdk.js +4 -1
- package/core/validation.js +13 -0
- package/dist/africode.js +858 -457
- package/dist/africode.js.map +14 -9
- package/dist/build-info.json +3 -3
- package/dist/components.js +784 -383
- package/dist/components.js.map +11 -6
- package/package.json +1 -1
- package/templates/starter/package.json +5 -5
- package/templates/starter/src/index.js +18 -0
- package/templates/starter/src/pages/index.js +1 -1
- package/templates/starter-3d/package.json +5 -5
- package/templates/starter-3d/src/pages/index.js +1 -1
- package/templates/starter-dashboard/.env.example +21 -0
- package/templates/starter-dashboard/africode.config.js +20 -0
- package/templates/starter-dashboard/package.json +14 -0
- package/templates/starter-dashboard/src/index.js +17 -0
- package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
- package/templates/starter-dashboard/src/pages/index.html +118 -0
- package/templates/starter-dashboard/src/pages/index.js +110 -0
- package/templates/starter-dashboard/src/styles/main.css +172 -0
- package/templates/starter-fintech/.env.example +28 -0
- package/templates/starter-fintech/africode.config.js +20 -0
- package/templates/starter-fintech/package.json +15 -0
- package/templates/starter-fintech/src/index.js +17 -0
- package/templates/starter-fintech/src/pages/api/auth.js +45 -0
- package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
- package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
- package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
- package/templates/starter-fintech/src/pages/index.html +169 -0
- package/templates/starter-fintech/src/pages/index.js +161 -0
- package/templates/starter-fintech/src/styles/main.css +246 -0
- package/templates/starter-react/package.json +5 -5
- package/templates/starter-react/src/pages/index.js +1 -1
- package/templates/starter-tailwind/package.json +5 -5
- package/templates/starter-tailwind/src/pages/index.js +1 -1
- package/templates/starter-website/.env.example +18 -0
- package/templates/starter-website/africode.config.js +20 -0
- package/templates/starter-website/package.json +14 -0
- package/templates/starter-website/src/index.js +17 -0
- package/templates/starter-website/src/pages/index.html +124 -0
- package/templates/starter-website/src/pages/index.js +116 -0
- package/templates/starter-website/src/styles/main.css +195 -0
package/core/patterns.js
CHANGED
|
@@ -575,6 +575,60 @@ export function generateZulu({
|
|
|
575
575
|
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Generate Swazi/Eswatini pattern
|
|
580
|
+
*/
|
|
581
|
+
export function generateSwazi({
|
|
582
|
+
primaryColor = '#003082', // Blue
|
|
583
|
+
secondaryColor = '#FCD116', // Yellow
|
|
584
|
+
accentColor = '#C8102E', // Red
|
|
585
|
+
backgroundColor = '#FFFFFF',
|
|
586
|
+
size = 50
|
|
587
|
+
} = {}) {
|
|
588
|
+
const svg = `
|
|
589
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
590
|
+
<rect width="${size}" height="${size}" fill="${backgroundColor}"/>
|
|
591
|
+
<!-- Swazi shield-like pattern -->
|
|
592
|
+
<path d="M${size * 0.5},0 C${size * 0.8},${size * 0.1} ${size},${size * 0.3} ${size},${size * 0.5} C${size},${size * 0.7} ${size * 0.8},${size * 0.9} ${size * 0.5},${size} C${size * 0.2},${size * 0.9} 0,${size * 0.7} 0,${size * 0.5} C0,${size * 0.3} ${size * 0.2},${size * 0.1} ${size * 0.5},0 Z" fill="${primaryColor}" opacity="0.8"/>
|
|
593
|
+
<!-- Diagonal stripes -->
|
|
594
|
+
<line x1="0" y1="${size * 0.2}" x2="${size * 0.2}" y2="0" stroke="${secondaryColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
595
|
+
<line x1="0" y1="${size * 0.4}" x2="${size * 0.4}" y2="0" stroke="${accentColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
596
|
+
<line x1="0" y1="${size * 0.6}" x2="${size * 0.6}" y2="0" stroke="${secondaryColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
597
|
+
<line x1="0" y1="${size * 0.8}" x2="${size * 0.8}" y2="0" stroke="${accentColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
598
|
+
<line x1="${size * 0.2}" y1="${size}" x2="${size}" y2="${size * 0.2}" stroke="${secondaryColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
599
|
+
<line x1="${size * 0.4}" y1="${size}" x2="${size}" y2="${size * 0.4}" stroke="${accentColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
600
|
+
<line x1="${size * 0.6}" y1="${size}" x2="${size}" y2="${size * 0.6}" stroke="${secondaryColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
601
|
+
<line x1="${size * 0.8}" y1="${size}" x2="${size}" y2="${size * 0.8}" stroke="${accentColor}" stroke-width="${size * 0.1}" stroke-opacity="0.7"/>
|
|
602
|
+
</svg>
|
|
603
|
+
`;
|
|
604
|
+
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Generate Xhosa pattern
|
|
609
|
+
*/
|
|
610
|
+
export function generateXhosa({
|
|
611
|
+
primaryColor = '#000000', // Black
|
|
612
|
+
secondaryColor = '#FFFFFF', // White
|
|
613
|
+
accentColor = '#FF0000', // Red
|
|
614
|
+
size = 50
|
|
615
|
+
} = {}) {
|
|
616
|
+
const svg = `
|
|
617
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
618
|
+
<rect width="${size}" height="${size}" fill="${secondaryColor}"/>
|
|
619
|
+
<!-- Xhosa geometric triangles -->
|
|
620
|
+
<polygon points="0,0 ${size * 0.5},${size * 0.5} 0,${size}" fill="${primaryColor}"/>
|
|
621
|
+
<polygon points="${size},0 ${size * 0.5},${size * 0.5} ${size},${size}" fill="${primaryColor}"/>
|
|
622
|
+
<!-- Center diamond -->
|
|
623
|
+
<polygon points="${size * 0.4},${size * 0.3} ${size * 0.5},${size * 0.2} ${size * 0.6},${size * 0.3} ${size * 0.5},${size * 0.4}" fill="${accentColor}"/>
|
|
624
|
+
<polygon points="${size * 0.4},${size * 0.7} ${size * 0.5},${size * 0.8} ${size * 0.6},${size * 0.7} ${size * 0.5},${size * 0.6}" fill="${accentColor}"/>
|
|
625
|
+
<!-- Horizontal band -->
|
|
626
|
+
<rect x="0" y="${size * 0.45}" width="${size}" height="${size * 0.1}" fill="${primaryColor}"/>
|
|
627
|
+
</svg>
|
|
628
|
+
`;
|
|
629
|
+
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
578
632
|
/**
|
|
579
633
|
* Generate Swazi/Eswatini pattern
|
|
580
634
|
* Inspired by Ligwalagwala (purple-crested turaco) and royal patterns
|
|
@@ -741,6 +795,8 @@ export default {
|
|
|
741
795
|
generateZuluBeads,
|
|
742
796
|
generateSwazi,
|
|
743
797
|
generateXhosa,
|
|
798
|
+
generateYorubaAdire,
|
|
799
|
+
generateAkanKente,
|
|
744
800
|
// Utilities
|
|
745
801
|
applyPattern,
|
|
746
802
|
// Tech Patterns
|
|
@@ -831,3 +887,115 @@ export function generateCircuit({
|
|
|
831
887
|
`;
|
|
832
888
|
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
833
889
|
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Generate Yoruba Adire pattern (Nigeria)
|
|
893
|
+
|
|
894
|
+
* Traditional resist-dyed cloth patterns
|
|
895
|
+
|
|
896
|
+
*/
|
|
897
|
+
|
|
898
|
+
export function generateYorubaAdire({
|
|
899
|
+
|
|
900
|
+
primaryColor = '#000000',
|
|
901
|
+
|
|
902
|
+
secondaryColor = '#FCD116',
|
|
903
|
+
|
|
904
|
+
backgroundColor = '#FFFFFF',
|
|
905
|
+
|
|
906
|
+
size = 50
|
|
907
|
+
|
|
908
|
+
} = {}) {
|
|
909
|
+
|
|
910
|
+
const svg = `
|
|
911
|
+
|
|
912
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
913
|
+
|
|
914
|
+
<rect width="${size}" height="${size}" fill="${backgroundColor}"/>
|
|
915
|
+
|
|
916
|
+
<!-- Adire tie-dye circles -->
|
|
917
|
+
|
|
918
|
+
<circle cx="${size * 0.2}" cy="${size * 0.2}" r="${size * 0.15}" fill="${primaryColor}"/>
|
|
919
|
+
|
|
920
|
+
<circle cx="${size * 0.8}" cy="${size * 0.2}" r="${size * 0.15}" fill="${primaryColor}"/>
|
|
921
|
+
|
|
922
|
+
<circle cx="${size * 0.2}" cy="${size * 0.8}" r="${size * 0.15}" fill="${primaryColor}"/>
|
|
923
|
+
|
|
924
|
+
<circle cx="${size * 0.8}" cy="${size * 0.8}" r="${size * 0.15}" fill="${primaryColor}"/>
|
|
925
|
+
|
|
926
|
+
<circle cx="${size * 0.5}" cy="${size * 0.5}" r="${size * 0.1}" fill="${secondaryColor}"/>
|
|
927
|
+
|
|
928
|
+
<!-- Connecting lines -->
|
|
929
|
+
|
|
930
|
+
<line x1="${size * 0.2}" y1="${size * 0.2}" x2="${size * 0.8}" y2="${size * 0.8}" stroke="${primaryColor}" stroke-width="2"/>
|
|
931
|
+
|
|
932
|
+
<line x1="${size * 0.8}" y1="${size * 0.2}" x2="${size * 0.2}" y2="${size * 0.8}" stroke="${primaryColor}" stroke-width="2"/>
|
|
933
|
+
|
|
934
|
+
</svg>
|
|
935
|
+
|
|
936
|
+
`;
|
|
937
|
+
|
|
938
|
+
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
939
|
+
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
|
|
944
|
+
* Generate Akan Kente pattern variation (Ghana)
|
|
945
|
+
|
|
946
|
+
* Geometric patterns with cultural significance
|
|
947
|
+
|
|
948
|
+
*/
|
|
949
|
+
|
|
950
|
+
export function generateAkanKente({
|
|
951
|
+
|
|
952
|
+
colors = ['#000000', '#FCD116', '#E82127', '#006B3F'],
|
|
953
|
+
|
|
954
|
+
size = 60
|
|
955
|
+
|
|
956
|
+
} = {}) {
|
|
957
|
+
|
|
958
|
+
const [black, gold, red, green] = colors;
|
|
959
|
+
|
|
960
|
+
const stripe = size / 6;
|
|
961
|
+
|
|
962
|
+
const svg = `
|
|
963
|
+
|
|
964
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
965
|
+
|
|
966
|
+
<rect width="${size}" height="${size}" fill="${gold}"/>
|
|
967
|
+
|
|
968
|
+
<!-- Horizontal black stripes -->
|
|
969
|
+
|
|
970
|
+
<rect x="0" y="0" width="${size}" height="${stripe}" fill="${black}"/>
|
|
971
|
+
|
|
972
|
+
<rect x="0" y="${stripe * 2}" width="${size}" height="${stripe}" fill="${black}"/>
|
|
973
|
+
|
|
974
|
+
<rect x="0" y="${stripe * 4}" width="${size}" height="${stripe}" fill="${black}"/>
|
|
975
|
+
|
|
976
|
+
<!-- Vertical red stripes -->
|
|
977
|
+
|
|
978
|
+
<rect x="0" y="0" width="${stripe}" height="${size}" fill="${red}" opacity="0.6"/>
|
|
979
|
+
|
|
980
|
+
<rect x="${stripe * 2}" y="0" width="${stripe}" height="${size}" fill="${red}" opacity="0.6"/>
|
|
981
|
+
|
|
982
|
+
<rect x="${stripe * 4}" y="0" width="${stripe}" height="${size}" fill="${red}" opacity="0.6"/>
|
|
983
|
+
|
|
984
|
+
<!-- Green corner squares -->
|
|
985
|
+
|
|
986
|
+
<rect x="0" y="0" width="${stripe * 0.8}" height="${stripe * 0.8}" fill="${green}"/>
|
|
987
|
+
|
|
988
|
+
<rect x="${size - stripe * 0.8}" y="0" width="${stripe * 0.8}" height="${stripe * 0.8}" fill="${green}"/>
|
|
989
|
+
|
|
990
|
+
<rect x="0" y="${size - stripe * 0.8}" width="${stripe * 0.8}" height="${stripe * 0.8}" fill="${green}"/>
|
|
991
|
+
|
|
992
|
+
<rect x="${size - stripe * 0.8}" y="${size - stripe * 0.8}" width="${stripe * 0.8}" height="${stripe * 0.8}" fill="${green}"/>
|
|
993
|
+
|
|
994
|
+
</svg>
|
|
995
|
+
|
|
996
|
+
`;
|
|
997
|
+
|
|
998
|
+
return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
|
|
999
|
+
|
|
1000
|
+
}
|
|
1001
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Request Identity System
|
|
3
|
+
*
|
|
4
|
+
* Identifies and tracks request origins, geolocation, and device characteristics
|
|
5
|
+
* to enhance security and analytics in the African digital economy context.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - IP geolocation detection (Africa-focused)
|
|
9
|
+
* - Device fingerprinting
|
|
10
|
+
* - Request origin tracking
|
|
11
|
+
* - Bot detection
|
|
12
|
+
* - Fraud pattern recognition
|
|
13
|
+
*
|
|
14
|
+
* @module core/request-identity
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { frameworkLog } from './logging.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* African countries ISO codes for geolocation
|
|
21
|
+
*/
|
|
22
|
+
const AFRICAN_COUNTRIES = [
|
|
23
|
+
'AO', 'BF', 'BI', 'BJ', 'BW', 'CD', 'CF', 'CG', 'CI', 'CM', 'CV', 'DJ',
|
|
24
|
+
'DZ', 'EG', 'EH', 'ER', 'ET', 'GA', 'GH', 'GM', 'GN', 'GQ', 'GW', 'KE',
|
|
25
|
+
'KM', 'LR', 'LS', 'LY', 'MA', 'MG', 'ML', 'MR', 'MU', 'MW', 'MZ', 'NA',
|
|
26
|
+
'NE', 'NG', 'RE', 'RW', 'SC', 'SD', 'SH', 'SL', 'SN', 'SO', 'SS', 'ST',
|
|
27
|
+
'SZ', 'TD', 'TG', 'TN', 'TZ', 'UG', 'YT', 'ZA', 'ZM', 'ZW'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect bot traffic based on common patterns
|
|
32
|
+
*/
|
|
33
|
+
function isBotUserAgent(userAgent) {
|
|
34
|
+
if (!userAgent) return false;
|
|
35
|
+
|
|
36
|
+
const botPatterns = [
|
|
37
|
+
/bot/i,
|
|
38
|
+
/crawl/i,
|
|
39
|
+
/spider/i,
|
|
40
|
+
/slurp/i,
|
|
41
|
+
/facebookexternalhit/i,
|
|
42
|
+
/linkedinbot/i,
|
|
43
|
+
/twitterbot/i,
|
|
44
|
+
/whatsapp/i,
|
|
45
|
+
/telegram/i
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
return botPatterns.some(pattern => pattern.test(userAgent));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract country from IP (simplified - in real implementation would use a service)
|
|
53
|
+
*/
|
|
54
|
+
function getCountryFromIP(ip) {
|
|
55
|
+
// This would normally call a geolocation service
|
|
56
|
+
// For now, return a mock implementation
|
|
57
|
+
// In production, integrate with services like ipapi.co or ipinfo.io
|
|
58
|
+
return 'UNKNOWN';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate request fingerprint
|
|
63
|
+
*/
|
|
64
|
+
function generateFingerprint(req) {
|
|
65
|
+
const headers = req.headers || new Headers();
|
|
66
|
+
const userAgent = headers.get('user-agent') || '';
|
|
67
|
+
const acceptLanguage = headers.get('accept-language') || '';
|
|
68
|
+
const acceptEncoding = headers.get('accept-encoding') || '';
|
|
69
|
+
|
|
70
|
+
// Get IP from various headers (considering proxies)
|
|
71
|
+
let ip = headers.get('x-forwarded-for') ||
|
|
72
|
+
headers.get('x-real-ip') ||
|
|
73
|
+
headers.get('x-client-ip') ||
|
|
74
|
+
(req.connection ? req.connection.remoteAddress : null) ||
|
|
75
|
+
(req.socket ? req.socket.remoteAddress : null) ||
|
|
76
|
+
(req.connection ? req.connection.socket.remoteAddress : null) ||
|
|
77
|
+
'127.0.0.1';
|
|
78
|
+
|
|
79
|
+
// Handle comma-separated IPs from x-forwarded-for
|
|
80
|
+
if (ip.includes(',')) {
|
|
81
|
+
ip = ip.split(',')[0].trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create a hash of the identifying characteristics
|
|
85
|
+
const fingerprintData = {
|
|
86
|
+
ip,
|
|
87
|
+
userAgent: sanitizeUserAgent(userAgent),
|
|
88
|
+
acceptLanguage,
|
|
89
|
+
acceptEncoding,
|
|
90
|
+
path: req.url || '',
|
|
91
|
+
method: req.method || 'GET'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return calculateHash(JSON.stringify(fingerprintData));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sanitize user agent to remove potentially identifying info
|
|
99
|
+
*/
|
|
100
|
+
function sanitizeUserAgent(userAgent) {
|
|
101
|
+
if (!userAgent) return '';
|
|
102
|
+
|
|
103
|
+
// Remove version numbers and build info for privacy
|
|
104
|
+
return userAgent
|
|
105
|
+
.replace(/\d+\.\d+(\.\d+)?/g, '[VERSION]')
|
|
106
|
+
.replace(/\([^\)]*\)/g, '(***REDACTED***)');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Simple hash function for fingerprinting
|
|
111
|
+
*/
|
|
112
|
+
function calculateHash(str) {
|
|
113
|
+
let hash = 0;
|
|
114
|
+
for (let i = 0; i < str.length; i++) {
|
|
115
|
+
const char = str.charCodeAt(i);
|
|
116
|
+
hash = ((hash << 5) - hash) + char;
|
|
117
|
+
hash |= 0; // Convert to 32bit integer
|
|
118
|
+
}
|
|
119
|
+
return Math.abs(hash).toString(36);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Enhanced logging with request identity
|
|
124
|
+
*/
|
|
125
|
+
export function requestIdentityMiddleware(options = {}) {
|
|
126
|
+
const {
|
|
127
|
+
logAllRequests = false,
|
|
128
|
+
logSuspicious = true,
|
|
129
|
+
logAfricanTraffic = true,
|
|
130
|
+
fraudDetection = true
|
|
131
|
+
} = options;
|
|
132
|
+
|
|
133
|
+
return async (ctx) => {
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
const fingerprint = generateFingerprint(ctx.req);
|
|
136
|
+
|
|
137
|
+
// Add identity info to context
|
|
138
|
+
ctx.identity = {
|
|
139
|
+
fingerprint,
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
userAgent: ctx.req.headers?.get('user-agent') || '',
|
|
142
|
+
ip: getIPFromRequest(ctx.req),
|
|
143
|
+
isBot: isBotUserAgent(ctx.req.headers?.get('user-agent')),
|
|
144
|
+
country: getCountryFromIP(getIPFromRequest(ctx.req)),
|
|
145
|
+
isAfrican: false, // Would be set based on geolocation
|
|
146
|
+
responseTime: 0
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Log request if configured
|
|
150
|
+
if (logAllRequests) {
|
|
151
|
+
frameworkLog('info', `[Request Identity] ${ctx.req.method} ${ctx.pathname} | Fingerprint: ${fingerprint}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Continue with request processing
|
|
155
|
+
const result = {};
|
|
156
|
+
|
|
157
|
+
// Set up response time calculation for post-processing
|
|
158
|
+
const originalEnd = ctx.res?.end;
|
|
159
|
+
if (originalEnd) {
|
|
160
|
+
ctx.res.end = function(...args) {
|
|
161
|
+
ctx.identity.responseTime = Date.now() - startTime;
|
|
162
|
+
if (ctx.identity.responseTime > 5000) { // Slow requests
|
|
163
|
+
frameworkLog('warn', `[Slow Request] ${ctx.pathname} took ${ctx.identity.responseTime}ms`);
|
|
164
|
+
}
|
|
165
|
+
return originalEnd.apply(this, args);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get IP from request considering various proxy headers
|
|
175
|
+
*/
|
|
176
|
+
function getIPFromRequest(req) {
|
|
177
|
+
const headers = req.headers || new Headers();
|
|
178
|
+
let ip = headers.get('x-forwarded-for') ||
|
|
179
|
+
headers.get('x-real-ip') ||
|
|
180
|
+
headers.get('x-client-ip') ||
|
|
181
|
+
(req.connection ? req.connection.remoteAddress : null) ||
|
|
182
|
+
(req.socket ? req.socket.remoteAddress : null) ||
|
|
183
|
+
(req.connection ? req.connection.socket.remoteAddress : null) ||
|
|
184
|
+
'127.0.0.1';
|
|
185
|
+
|
|
186
|
+
// Handle comma-separated IPs from x-forwarded-for
|
|
187
|
+
if (ip.includes(',')) {
|
|
188
|
+
ip = ip.split(',')[0].trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return ip;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fraud detection middleware
|
|
196
|
+
*/
|
|
197
|
+
export function fraudDetectionMiddleware(options = {}) {
|
|
198
|
+
const {
|
|
199
|
+
maxRequestsPerMinute = 60,
|
|
200
|
+
suspiciousPatterns = [],
|
|
201
|
+
blockBots = true
|
|
202
|
+
} = options;
|
|
203
|
+
|
|
204
|
+
// In-memory store for request tracking (would use Redis in production)
|
|
205
|
+
const requestCounts = new Map();
|
|
206
|
+
|
|
207
|
+
return async (ctx) => {
|
|
208
|
+
const ip = getIPFromRequest(ctx.req);
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const minuteKey = `${ip}:${Math.floor(now / 60000)}`;
|
|
211
|
+
|
|
212
|
+
// Track requests per minute
|
|
213
|
+
if (!requestCounts.has(minuteKey)) {
|
|
214
|
+
requestCounts.set(minuteKey, 0);
|
|
215
|
+
}
|
|
216
|
+
requestCounts.set(minuteKey, requestCounts.get(minuteKey) + 1);
|
|
217
|
+
|
|
218
|
+
const requestCount = requestCounts.get(minuteKey);
|
|
219
|
+
|
|
220
|
+
// Check for excessive requests
|
|
221
|
+
if (requestCount > maxRequestsPerMinute) {
|
|
222
|
+
frameworkLog('warn', `[Fraud Detection] Potential DoS from IP: ${ip}, requests: ${requestCount}`);
|
|
223
|
+
return {
|
|
224
|
+
response: new Response('Too many requests', { status: 429 })
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check for bot traffic if configured
|
|
229
|
+
if (blockBots && isBotUserAgent(ctx.req.headers?.get('user-agent'))) {
|
|
230
|
+
frameworkLog('info', `[Bot Traffic] Blocked bot: ${ctx.req.headers?.get('user-agent')} from ${ip}`);
|
|
231
|
+
return {
|
|
232
|
+
response: new Response('Bot traffic not allowed', { status: 403 })
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add identity info to context
|
|
237
|
+
ctx.identity = ctx.identity || {};
|
|
238
|
+
Object.assign(ctx.identity, {
|
|
239
|
+
ip,
|
|
240
|
+
isBot: isBotUserAgent(ctx.req.headers?.get('user-agent')),
|
|
241
|
+
requestCount
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {};
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default {
|
|
249
|
+
requestIdentityMiddleware,
|
|
250
|
+
fraudDetectionMiddleware,
|
|
251
|
+
generateFingerprint,
|
|
252
|
+
getIPFromRequest,
|
|
253
|
+
isBotUserAgent
|
|
254
|
+
};
|
package/core/request-identity.js
CHANGED
|
@@ -1,44 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request
|
|
3
|
-
*
|
|
2
|
+
* AfriCode Request Identity Utilities
|
|
3
|
+
*
|
|
4
|
+
* Core utilities for handling request identity, cookies, and session management
|
|
5
|
+
* that are referenced by the SDK
|
|
6
|
+
*
|
|
4
7
|
* @module core/request-identity
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return cookies;
|
|
25
|
-
}, {});
|
|
10
|
+
/**
|
|
11
|
+
* Parse cookie header into key-value pairs
|
|
12
|
+
* @param {string} cookieHeader - The Cookie header value
|
|
13
|
+
* @returns {Object} Parsed cookies as key-value pairs
|
|
14
|
+
*/
|
|
15
|
+
export function parseCookieHeader(cookieHeader) {
|
|
16
|
+
if (!cookieHeader) return {};
|
|
17
|
+
|
|
18
|
+
const cookies = {};
|
|
19
|
+
cookieHeader.split(';').forEach(cookie => {
|
|
20
|
+
const [name, value] = cookie.trim().split('=');
|
|
21
|
+
if (name && value) {
|
|
22
|
+
cookies[decodeURIComponent(name)] = decodeURIComponent(value);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return cookies;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Create a secure session ID
|
|
31
|
+
* @returns {string} A cryptographically secure session ID
|
|
32
|
+
*/
|
|
28
33
|
export function createSessionId() {
|
|
34
|
+
// Use crypto.randomUUID() for secure UUID generation
|
|
29
35
|
return crypto.randomUUID();
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Get request identity (session ID from cookies)
|
|
40
|
+
* @param {Request} request - The incoming request
|
|
41
|
+
* @returns {string|null} Session ID from cookie
|
|
42
|
+
*/
|
|
43
|
+
export function getRequestIdentity(request) {
|
|
44
|
+
const headers = request.headers || {};
|
|
45
|
+
const cookieHeader = headers.get('cookie');
|
|
34
46
|
const cookies = parseCookieHeader(cookieHeader);
|
|
35
|
-
|
|
47
|
+
return cookies[SESSION_COOKIE_NAME] || null;
|
|
48
|
+
}
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Get full request identity information
|
|
52
|
+
* @param {Request} request - The incoming request
|
|
53
|
+
* @returns {Object} Detailed request identity information
|
|
54
|
+
*/
|
|
55
|
+
export function getFullRequestIdentity(request) {
|
|
56
|
+
const headers = request.headers || {};
|
|
57
|
+
const userAgent = headers.get('user-agent') || '';
|
|
58
|
+
const forwardedFor = headers.get('x-forwarded-for');
|
|
59
|
+
const realIp = headers.get('x-real-ip') || headers.get('x-client-ip');
|
|
60
|
+
const cfConnectingIp = headers.get('cf-connecting-ip');
|
|
61
|
+
const xClusterClientIp = headers.get('x-cluster-client-ip');
|
|
62
|
+
|
|
63
|
+
let ip = realIp || forwardedFor?.split(',')[0] || cfConnectingIp || xClusterClientIp;
|
|
64
|
+
if (!ip) {
|
|
65
|
+
ip = request.connection?.remoteAddress || request.socket?.remoteAddress;
|
|
39
66
|
}
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
if (!ip) ip = '127.0.0.1';
|
|
68
|
+
if (ip.startsWith('::ffff:')) {
|
|
69
|
+
ip = ip.substring(7);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
ip,
|
|
74
|
+
userAgent,
|
|
75
|
+
sessionId: parseCookieHeader(headers.get('cookie'))[SESSION_COOKIE_NAME] || null,
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
method: request.method || 'GET',
|
|
78
|
+
url: request.url || '',
|
|
79
|
+
referer: headers.get('referer') || headers.get('referrer') || '',
|
|
80
|
+
language: headers.get('accept-language') || ''
|
|
81
|
+
};
|
|
42
82
|
}
|
|
43
83
|
|
|
44
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Constant for session cookie name
|
|
86
|
+
*/
|
|
87
|
+
export const SESSION_COOKIE_NAME = 'session_id';
|
|
88
|
+
|
|
89
|
+
export default {
|
|
90
|
+
parseCookieHeader,
|
|
91
|
+
createSessionId,
|
|
92
|
+
getRequestIdentity,
|
|
93
|
+
SESSION_COOKIE_NAME
|
|
94
|
+
};
|
package/core/sdk.js
CHANGED
|
@@ -17,6 +17,7 @@ import { getRequestIdentity, parseCookieHeader, createSessionId, SESSION_COOKIE_
|
|
|
17
17
|
import { sessionStore } from './session-store.js';
|
|
18
18
|
import { actions as runtimeActions } from './actions.js';
|
|
19
19
|
import { MiddlewareManager, loggerMiddleware, authMiddleware } from './middleware.js';
|
|
20
|
+
import * as requestAnalytics from './request-analytics.js';
|
|
20
21
|
import {
|
|
21
22
|
AfriCodeError, ValidationError, InvalidUrlError,
|
|
22
23
|
SecurityError, CsrfError, RateLimitError,
|
|
@@ -84,7 +85,8 @@ export {
|
|
|
84
85
|
CsrfError,
|
|
85
86
|
RateLimitError,
|
|
86
87
|
DatabaseError,
|
|
87
|
-
ComponentError
|
|
88
|
+
ComponentError,
|
|
89
|
+
requestAnalytics
|
|
88
90
|
};
|
|
89
91
|
|
|
90
92
|
// v5.0.0 exports
|
|
@@ -141,5 +143,6 @@ export default {
|
|
|
141
143
|
RateLimitError,
|
|
142
144
|
DatabaseError,
|
|
143
145
|
ComponentError,
|
|
146
|
+
requestAnalytics,
|
|
144
147
|
init
|
|
145
148
|
};
|
package/core/validation.js
CHANGED
|
@@ -372,6 +372,19 @@ export class AfriFieldBuilder {
|
|
|
372
372
|
*/
|
|
373
373
|
message(message) {
|
|
374
374
|
this._customMessage = message;
|
|
375
|
+
// Update the base schema with the custom message if applicable
|
|
376
|
+
if (this._customMessage && this._baseSchema) {
|
|
377
|
+
// For string schemas with min validation
|
|
378
|
+
if (this._fieldType === 'string' && this._baseSchema._def?.minLength) {
|
|
379
|
+
this._baseSchema = z.string().min(this._baseSchema._def.minLength.value, message);
|
|
380
|
+
} else if (this._fieldType === 'url') {
|
|
381
|
+
this._baseSchema = z.string().min(1, 'URL cannot be empty').url(message);
|
|
382
|
+
} else if (this._fieldType === 'email') {
|
|
383
|
+
this._baseSchema = z.string().min(1, 'Email cannot be empty').email(message);
|
|
384
|
+
} else if (this._fieldType === 'phone') {
|
|
385
|
+
this._baseSchema = z.string().regex(/^[\+]?[1-9][\d]{0,15}$/, message);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
375
388
|
return this;
|
|
376
389
|
}
|
|
377
390
|
|