@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
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
+ };
@@ -1,44 +1,94 @@
1
1
  /**
2
- * Request identity resolver for AfriCode sessions.
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
- const SESSION_COOKIE_NAME = 'afri_session';
8
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
-
10
- export function parseCookieHeader(cookieHeader = '') {
11
- return cookieHeader
12
- .split(';')
13
- .map((part) => part.trim())
14
- .filter(Boolean)
15
- .reduce((cookies, part) => {
16
- const separatorIndex = part.indexOf('=');
17
- if (separatorIndex === -1) {
18
- return cookies;
19
- }
20
-
21
- const key = part.slice(0, separatorIndex).trim();
22
- const value = part.slice(separatorIndex + 1).trim();
23
- cookies[key] = value;
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
- export function getRequestIdentity(req) {
33
- const cookieHeader = req?.headers?.get?.('cookie') || '';
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
- const sessionId = cookies[SESSION_COOKIE_NAME];
47
+ return cookies[SESSION_COOKIE_NAME] || null;
48
+ }
36
49
 
37
- if (sessionId && UUID_PATTERN.test(sessionId)) {
38
- return sessionId;
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
- return createSessionId();
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
- export { SESSION_COOKIE_NAME };
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
  };
@@ -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