@africode/core 5.0.0

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 (136) hide show
  1. package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
  2. package/LICENSE +623 -0
  3. package/README.md +442 -0
  4. package/bin/africode.js +73 -0
  5. package/bin/africode.js.1758507140 +343 -0
  6. package/bin/cli.ts +83 -0
  7. package/bin/create-africode.js +158 -0
  8. package/bin/scaffold.ts +219 -0
  9. package/components/accordion.js +183 -0
  10. package/components/alert.js +131 -0
  11. package/components/auth.js +172 -0
  12. package/components/avatar.js +117 -0
  13. package/components/badge.js +104 -0
  14. package/components/base.d.ts +139 -0
  15. package/components/base.js +184 -0
  16. package/components/button.js +164 -0
  17. package/components/card.js +137 -0
  18. package/components/cultural-card.js +243 -0
  19. package/components/divider.js +83 -0
  20. package/components/dropdown.js +171 -0
  21. package/components/error-boundary.js +155 -0
  22. package/components/form.js +131 -0
  23. package/components/grid.js +273 -0
  24. package/components/hero.js +138 -0
  25. package/components/icon.js +36 -0
  26. package/components/index.js +57 -0
  27. package/components/input.js +256 -0
  28. package/components/kanga-card.js +185 -0
  29. package/components/language-switcher.js +108 -0
  30. package/components/loader.js +80 -0
  31. package/components/modal.js +262 -0
  32. package/components/motion.js +84 -0
  33. package/components/navbar.js +236 -0
  34. package/components/pattern-showcase.js +225 -0
  35. package/components/progress.js +134 -0
  36. package/components/react.js +111 -0
  37. package/components/section.js +54 -0
  38. package/components/select.js +322 -0
  39. package/components/sidebar.js +180 -0
  40. package/components/skeleton.js +85 -0
  41. package/components/table.js +181 -0
  42. package/components/tabs.js +202 -0
  43. package/components/theme-toggle.js +82 -0
  44. package/components/toast.js +139 -0
  45. package/components/tooltip.js +167 -0
  46. package/core/a2ui-schema-manager.js +344 -0
  47. package/core/a2ui.js +431 -0
  48. package/core/bun-runtime.js +799 -0
  49. package/core/cli/commands/add.js +23 -0
  50. package/core/cli/commands/audit.js +58 -0
  51. package/core/cli/commands/build.js +137 -0
  52. package/core/cli/commands/create-plugin.js +241 -0
  53. package/core/cli/commands/dev.js +228 -0
  54. package/core/cli/commands/lint.js +23 -0
  55. package/core/cli/commands/test.js +34 -0
  56. package/core/cli/migrator.js +71 -0
  57. package/core/cli/ui.js +46 -0
  58. package/core/compliance.js +628 -0
  59. package/core/config.js +263 -0
  60. package/core/db-advanced.js +481 -0
  61. package/core/db.js +284 -0
  62. package/core/enhanced-hmr.js +404 -0
  63. package/core/errors.js +222 -0
  64. package/core/file-router.js +290 -0
  65. package/core/heartbeat.js +64 -0
  66. package/core/hmr-client.js +204 -0
  67. package/core/hmr.js +196 -0
  68. package/core/html.d.ts +116 -0
  69. package/core/html.js +160 -0
  70. package/core/hydration.js +52 -0
  71. package/core/lipa-namba-journey.js +572 -0
  72. package/core/motion.js +106 -0
  73. package/core/nida-cig-middleware.js +455 -0
  74. package/core/patterns.d.ts +124 -0
  75. package/core/patterns.js +833 -0
  76. package/core/plugins/index.js +312 -0
  77. package/core/router.js +387 -0
  78. package/core/sdk-client.js +62 -0
  79. package/core/sdk.d.ts +133 -0
  80. package/core/sdk.js +123 -0
  81. package/core/seo.js +76 -0
  82. package/core/server/auth-endpoints.js +339 -0
  83. package/core/server/auth.js +180 -0
  84. package/core/server/csrf.js +206 -0
  85. package/core/server/db.js +39 -0
  86. package/core/server/middleware.js +324 -0
  87. package/core/server/rate-limit.js +238 -0
  88. package/core/server/render.js +69 -0
  89. package/core/server/router.js +120 -0
  90. package/core/shim.js +28 -0
  91. package/core/state.d.ts +86 -0
  92. package/core/state.js +242 -0
  93. package/core/store.d.ts +122 -0
  94. package/core/store.js +61 -0
  95. package/core/validation.d.ts +233 -0
  96. package/core/validation.js +590 -0
  97. package/core/websocket.js +639 -0
  98. package/dist/africode.js +2905 -0
  99. package/dist/africode.js.map +61 -0
  100. package/dist/build-info.json +23 -0
  101. package/dist/components.js +2888 -0
  102. package/dist/components.js.map +58 -0
  103. package/dist/styles/africanity.css +322 -0
  104. package/dist/styles/typography.css +141 -0
  105. package/docs/IDE-Guide.md +50 -0
  106. package/package.json +110 -0
  107. package/src/index.ts +196 -0
  108. package/styles/africanity.css +322 -0
  109. package/styles/typography.css +141 -0
  110. package/templates/starter/.env.example +15 -0
  111. package/templates/starter/africode.config.js +40 -0
  112. package/templates/starter/package.json +14 -0
  113. package/templates/starter/src/pages/index.html +46 -0
  114. package/templates/starter/src/pages/index.js +32 -0
  115. package/templates/starter/src/styles/main.css +4 -0
  116. package/templates/starter-3d/.env.example +7 -0
  117. package/templates/starter-3d/africode.config.js +29 -0
  118. package/templates/starter-3d/components/af-model-viewer.js +125 -0
  119. package/templates/starter-3d/package.json +15 -0
  120. package/templates/starter-3d/src/pages/index.html +46 -0
  121. package/templates/starter-3d/src/pages/index.js +50 -0
  122. package/templates/starter-3d/src/styles/main.css +4 -0
  123. package/templates/starter-react/.env.example +15 -0
  124. package/templates/starter-react/africode.config.js +40 -0
  125. package/templates/starter-react/package.json +16 -0
  126. package/templates/starter-react/src/pages/index.html +46 -0
  127. package/templates/starter-react/src/pages/index.js +68 -0
  128. package/templates/starter-react/src/styles/main.css +4 -0
  129. package/templates/starter-tailwind/.env.example +15 -0
  130. package/templates/starter-tailwind/africode.config.js +40 -0
  131. package/templates/starter-tailwind/package.json +20 -0
  132. package/templates/starter-tailwind/src/pages/index.html +46 -0
  133. package/templates/starter-tailwind/src/pages/index.js +37 -0
  134. package/templates/starter-tailwind/src/styles/main.css +4 -0
  135. package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
  136. package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
package/core/errors.js ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * AfriCode Error System
3
+ *
4
+ * Standardized error hierarchy for the entire framework.
5
+ * Every error has: code, message, field (optional), context (optional).
6
+ *
7
+ * Design:
8
+ * - All framework errors extend AfriCodeError
9
+ * - Errors carry structured metadata for programmatic handling
10
+ * - toJSON() produces a consistent API response format
11
+ * - Error codes are namespaced: VALIDATION_*, SECURITY_*, DB_*, etc.
12
+ *
13
+ * @module core/errors
14
+ */
15
+
16
+ /**
17
+ * Base error class for all AfriCode framework errors.
18
+ *
19
+ * @example
20
+ * throw new AfriCodeError('Something failed', 'GENERIC_ERROR', { detail: '...' });
21
+ *
22
+ * @example
23
+ * // In an API handler:
24
+ * try { ... } catch (err) {
25
+ * if (err instanceof AfriCodeError) {
26
+ * return new Response(JSON.stringify(err.toJSON()), { status: err.statusCode });
27
+ * }
28
+ * }
29
+ */
30
+ export class AfriCodeError extends Error {
31
+ /**
32
+ * @param {string} message - Human-readable error message
33
+ * @param {string} code - Machine-readable error code (e.g. 'VALIDATION_FAILED')
34
+ * @param {Object} [context={}] - Additional context for debugging
35
+ * @param {number} [statusCode=500] - HTTP status code
36
+ */
37
+ constructor(message, code = 'INTERNAL_ERROR', context = {}, statusCode = 500) {
38
+ super(message);
39
+ this.name = 'AfriCodeError';
40
+ this.code = code;
41
+ this.context = context;
42
+ this.statusCode = statusCode;
43
+ this.timestamp = new Date().toISOString();
44
+ }
45
+
46
+ /**
47
+ * Produce a consistent JSON structure for API responses.
48
+ * @returns {Object}
49
+ */
50
+ toJSON() {
51
+ return {
52
+ error: {
53
+ code: this.code,
54
+ message: this.message,
55
+ ...(this.context.field ? { field: this.context.field } : {}),
56
+ ...(Object.keys(this.context).length > 0 ? { context: this.context } : {})
57
+ }
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Create an HTTP Response from this error.
63
+ * @returns {Response}
64
+ */
65
+ toResponse() {
66
+ return new Response(JSON.stringify(this.toJSON()), {
67
+ status: this.statusCode,
68
+ headers: { 'Content-Type': 'application/json' }
69
+ });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Validation error — thrown when input fails schema validation.
75
+ *
76
+ * @example
77
+ * throw new ValidationError("Field 'avatar_url' must be a valid URL or null", 'avatar_url');
78
+ */
79
+ export class ValidationError extends AfriCodeError {
80
+ /**
81
+ * @param {string} message
82
+ * @param {string} [field] - The field that failed validation
83
+ * @param {Object} [context={}]
84
+ */
85
+ constructor(message, field = null, context = {}) {
86
+ super(message, 'VALIDATION_FAILED', { field, ...context }, 422);
87
+ this.name = 'ValidationError';
88
+ }
89
+
90
+ /**
91
+ * Create a ValidationError from a Zod error.
92
+ * @param {import('zod').ZodError} zodError
93
+ * @returns {ValidationError}
94
+ */
95
+ static fromZodError(zodError) {
96
+ const firstIssue = zodError.issues[0];
97
+ const field = firstIssue?.path?.join('.') || null;
98
+ const message = firstIssue?.message || 'Validation failed';
99
+
100
+ return new ValidationError(message, field, {
101
+ issues: zodError.issues.map(issue => ({
102
+ field: issue.path.join('.'),
103
+ message: issue.message,
104
+ code: issue.code
105
+ }))
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Invalid URL error — specific subtype of validation error for URL fields.
112
+ *
113
+ * @example
114
+ * throw new InvalidUrlError('avatar_url', '');
115
+ * // → { error: { code: "INVALID_URL", message: "Field 'avatar_url' ...", field: "avatar_url" } }
116
+ */
117
+ export class InvalidUrlError extends AfriCodeError {
118
+ /**
119
+ * @param {string} field - The URL field name
120
+ * @param {*} value - The invalid value that was provided
121
+ */
122
+ constructor(field, value) {
123
+ const valueDesc = value === '' ? 'an empty string' : `"${value}"`;
124
+ super(
125
+ `Field '${field}' must be a valid URL or null. Got ${valueDesc}.`,
126
+ 'INVALID_URL',
127
+ { field, received: value },
128
+ 422
129
+ );
130
+ this.name = 'InvalidUrlError';
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Security error — thrown for CSRF violations, rate limiting, etc.
136
+ */
137
+ export class SecurityError extends AfriCodeError {
138
+ /**
139
+ * @param {string} message
140
+ * @param {string} [code='SECURITY_VIOLATION']
141
+ * @param {Object} [context={}]
142
+ * @param {number} [statusCode=403]
143
+ */
144
+ constructor(message, code = 'SECURITY_VIOLATION', context = {}, statusCode = 403) {
145
+ super(message, code, context, statusCode);
146
+ this.name = 'SecurityError';
147
+ }
148
+ }
149
+
150
+ /**
151
+ * CSRF error — thrown when CSRF token is missing or mismatched.
152
+ */
153
+ export class CsrfError extends SecurityError {
154
+ constructor(reason = 'missing') {
155
+ const messages = {
156
+ missing: 'CSRF token missing. Include the token in both the cookie and the X-CSRF-Token header.',
157
+ mismatch: 'CSRF token mismatch. The header token does not match the cookie token.'
158
+ };
159
+ super(messages[reason] || messages.missing, 'CSRF_FAILURE', { reason });
160
+ this.name = 'CsrfError';
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Rate limit error — thrown when request limit is exceeded.
166
+ */
167
+ export class RateLimitError extends SecurityError {
168
+ /**
169
+ * @param {number} retryAfter - Seconds until the client can retry
170
+ * @param {string} [message]
171
+ */
172
+ constructor(retryAfter, message = 'Too many requests. Please try again later.') {
173
+ super(message, 'RATE_LIMIT_EXCEEDED', { retryAfter }, 429);
174
+ this.name = 'RateLimitError';
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Database error — thrown for DB operation failures.
180
+ */
181
+ export class DatabaseError extends AfriCodeError {
182
+ /**
183
+ * @param {string} message
184
+ * @param {string} [operation] - The operation that failed (e.g. 'INSERT', 'UPDATE')
185
+ * @param {Object} [context={}]
186
+ */
187
+ constructor(message, operation = null, context = {}) {
188
+ super(message, 'DATABASE_ERROR', { operation, ...context }, 500);
189
+ this.name = 'DatabaseError';
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Component error — thrown for Web Component failures.
195
+ */
196
+ export class ComponentError extends AfriCodeError {
197
+ /**
198
+ * @param {string} message
199
+ * @param {string} componentName - The component tag name (e.g. 'af-button')
200
+ * @param {Object} [context={}]
201
+ */
202
+ constructor(message, componentName, context = {}) {
203
+ super(
204
+ `Component Error in <${componentName}>: ${message}`,
205
+ 'COMPONENT_ERROR',
206
+ { componentName, ...context },
207
+ 500
208
+ );
209
+ this.name = 'ComponentError';
210
+ }
211
+ }
212
+
213
+ export default {
214
+ AfriCodeError,
215
+ ValidationError,
216
+ InvalidUrlError,
217
+ SecurityError,
218
+ CsrfError,
219
+ RateLimitError,
220
+ DatabaseError,
221
+ ComponentError
222
+ };
@@ -0,0 +1,290 @@
1
+ /**
2
+ * File-Based Router for Bun Runtime
3
+ * Automatic mapping of /pages directory to URLs
4
+ * Supports dynamic routes, nested layouts, and API routes
5
+ */
6
+
7
+ import { readdirSync, statSync } from 'fs';
8
+ import { join, extname, dirname, relative } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ export class FileBasedRouter {
12
+ constructor(options = {}) {
13
+ this.options = {
14
+ pagesDir: options.pagesDir || 'pages',
15
+ apiDir: options.apiDir || 'pages/api',
16
+ layoutsDir: options.layoutsDir || 'layouts',
17
+ middlewareDir: options.middlewareDir || 'middleware',
18
+ ...options
19
+ };
20
+
21
+ this.routes = new Map();
22
+ this.apiRoutes = new Map();
23
+ this.layouts = new Map();
24
+ this.middleware = new Map();
25
+
26
+ this._scanRoutes();
27
+ }
28
+
29
+ /**
30
+ * Scan pages directory and build route mappings
31
+ */
32
+ _scanRoutes() {
33
+ const pagesPath = join(process.cwd(), this.options.pagesDir);
34
+
35
+ try {
36
+ this._scanDirectory(pagesPath, '');
37
+ } catch (error) {
38
+ console.warn('[Router] Pages directory not found:', pagesPath);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Recursively scan directory for route files
44
+ */
45
+ _scanDirectory(dirPath, routePrefix) {
46
+ const items = readdirSync(dirPath);
47
+
48
+ for (const item of items) {
49
+ const itemPath = join(dirPath, item);
50
+ const stat = statSync(itemPath);
51
+
52
+ if (stat.isDirectory()) {
53
+ // Handle nested directories
54
+ const subRoute = routePrefix + '/' + item;
55
+ this._scanDirectory(itemPath, subRoute);
56
+ } else if (stat.isFile()) {
57
+ // Handle route files
58
+ this._processRouteFile(itemPath, item, routePrefix);
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Process individual route file
65
+ */
66
+ _processRouteFile(filePath, filename, routePrefix) {
67
+ const ext = extname(filename);
68
+ const name = filename.replace(ext, '');
69
+
70
+ // Skip non-route files
71
+ if (!['.js', '.ts', '.mjs'].includes(ext)) return;
72
+
73
+ // Handle API routes
74
+ if (filePath.includes('/api/') || routePrefix.includes('/api')) {
75
+ this._addApiRoute(filePath, filename, routePrefix);
76
+ return;
77
+ }
78
+
79
+ // Handle page routes
80
+ this._addPageRoute(filePath, filename, routePrefix);
81
+ }
82
+
83
+ /**
84
+ * Add API route mapping
85
+ */
86
+ _addApiRoute(filePath, filename, routePrefix) {
87
+ let routePath = routePrefix + '/' + filename.replace(/\.(js|ts|mjs)$/, '');
88
+
89
+ // Handle dynamic routes
90
+ routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
91
+
92
+ // Handle catch-all routes
93
+ routePath = routePath.replace(/\[\.\.\.([^\]]+)\]/g, '*$1');
94
+
95
+ this.apiRoutes.set(routePath, {
96
+ filePath,
97
+ handler: null, // Lazy load
98
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
99
+ });
100
+
101
+ console.log(`[Router] API Route: ${routePath} -> ${relative(process.cwd(), filePath)}`);
102
+ }
103
+
104
+ /**
105
+ * Add page route mapping
106
+ */
107
+ _addPageRoute(filePath, filename, routePrefix) {
108
+ let routePath = routePrefix;
109
+
110
+ // Handle index files
111
+ if (filename === 'index.js' || filename === 'index.ts' || filename === 'index.mjs') {
112
+ // Keep routePrefix as is
113
+ } else {
114
+ routePath += '/' + filename.replace(/\.(js|ts|mjs)$/, '');
115
+ }
116
+
117
+ // Handle dynamic routes
118
+ routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
119
+
120
+ // Handle catch-all routes
121
+ routePath = routePath.replace(/\[\.\.\.([^\]]+)\]/g, '*$1');
122
+
123
+ // Ensure root route
124
+ if (routePath === '') routePath = '/';
125
+
126
+ this.routes.set(routePath, {
127
+ filePath,
128
+ component: null, // Lazy load
129
+ layout: this._findLayoutForRoute(routePath)
130
+ });
131
+
132
+ console.log(`[Router] Page Route: ${routePath} -> ${relative(process.cwd(), filePath)}`);
133
+ }
134
+
135
+ /**
136
+ * Find appropriate layout for route
137
+ */
138
+ _findLayoutForRoute(routePath) {
139
+ // Check for nested layouts
140
+ const segments = routePath.split('/').filter(Boolean);
141
+ let layoutPath = null;
142
+
143
+ // Walk up the route hierarchy
144
+ for (let i = segments.length; i >= 0; i--) {
145
+ const testPath = '/' + segments.slice(0, i).join('/');
146
+ const layoutFile = join(process.cwd(), this.options.layoutsDir, testPath, 'layout.js');
147
+
148
+ try {
149
+ statSync(layoutFile);
150
+ layoutPath = layoutFile;
151
+ break;
152
+ } catch {
153
+ // Continue searching
154
+ }
155
+ }
156
+
157
+ // Fallback to root layout
158
+ if (!layoutPath) {
159
+ const rootLayout = join(process.cwd(), this.options.layoutsDir, 'layout.js');
160
+ try {
161
+ statSync(rootLayout);
162
+ layoutPath = rootLayout;
163
+ } catch {
164
+ // No layout found
165
+ }
166
+ }
167
+
168
+ return layoutPath;
169
+ }
170
+
171
+ /**
172
+ * Match incoming request to route
173
+ */
174
+ matchRoute(pathname, method = 'GET') {
175
+ // Check API routes first
176
+ for (const [routePath, route] of this.apiRoutes) {
177
+ const params = this._matchRoutePattern(routePath, pathname);
178
+ if (params && route.methods.includes(method)) {
179
+ return {
180
+ type: 'api',
181
+ route: routePath,
182
+ filePath: route.filePath,
183
+ params,
184
+ handler: route.handler
185
+ };
186
+ }
187
+ }
188
+
189
+ // Check page routes
190
+ for (const [routePath, route] of this.routes) {
191
+ const params = this._matchRoutePattern(routePath, pathname);
192
+ if (params) {
193
+ return {
194
+ type: 'page',
195
+ route: routePath,
196
+ filePath: route.filePath,
197
+ params,
198
+ component: route.component,
199
+ layout: route.layout
200
+ };
201
+ }
202
+ }
203
+
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Match route pattern with pathname
209
+ */
210
+ _matchRoutePattern(pattern, pathname) {
211
+ const patternParts = pattern.split('/').filter(Boolean);
212
+ const pathParts = pathname.split('/').filter(Boolean);
213
+
214
+ if (pattern === '/' && pathname === '/') {
215
+ return {};
216
+ }
217
+
218
+ if (patternParts.length !== pathParts.length && !pattern.includes('*')) {
219
+ return null;
220
+ }
221
+
222
+ const params = {};
223
+
224
+ for (let i = 0; i < patternParts.length; i++) {
225
+ const patternPart = patternParts[i];
226
+ const pathPart = pathParts[i];
227
+
228
+ if (patternPart.startsWith(':')) {
229
+ // Dynamic parameter
230
+ const paramName = patternPart.slice(1);
231
+ params[paramName] = pathPart;
232
+ } else if (patternPart.startsWith('*')) {
233
+ // Catch-all parameter
234
+ const paramName = patternPart.slice(1);
235
+ params[paramName] = pathParts.slice(i).join('/');
236
+ return params; // Catch-all consumes rest
237
+ } else if (patternPart !== pathPart) {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ return params;
243
+ }
244
+
245
+ /**
246
+ * Load route handler/component lazily
247
+ */
248
+ async loadRoute(routeMatch) {
249
+ if (routeMatch.type === 'api') {
250
+ if (!routeMatch.handler) {
251
+ const module = await import(routeMatch.filePath);
252
+ routeMatch.handler = module.default || module;
253
+ this.apiRoutes.get(routeMatch.route).handler = routeMatch.handler;
254
+ }
255
+ return routeMatch.handler;
256
+ } else {
257
+ if (!routeMatch.component) {
258
+ const module = await import(routeMatch.filePath);
259
+ routeMatch.component = module.default || module;
260
+ this.routes.get(routeMatch.route).component = routeMatch.component;
261
+ }
262
+ return routeMatch.component;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get all registered routes
268
+ */
269
+ getRoutes() {
270
+ return {
271
+ pages: Array.from(this.routes.keys()),
272
+ apis: Array.from(this.apiRoutes.keys())
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Clear route cache (for hot reloading)
278
+ */
279
+ clearCache() {
280
+ for (const route of this.apiRoutes.values()) {
281
+ route.handler = null;
282
+ }
283
+ for (const route of this.routes.values()) {
284
+ route.component = null;
285
+ }
286
+ console.log('[Router] Route cache cleared');
287
+ }
288
+ }
289
+
290
+ export default FileBasedRouter;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * AfriCode Heartbeat (Integrity System)
3
+ *
4
+ * "Unremovable" integrity check that logs usage and validates
5
+ * the framework environment.
6
+ *
7
+ * @module core/heartbeat
8
+ */
9
+
10
+ const HEARTBEAT_KEY = 'af_integrity';
11
+ const FRAMEWORK_VERSION = '1.0.0';
12
+
13
+ export function initHeartbeat() {
14
+ // 1. Console Stamp
15
+ console.log(
16
+ `%c AfriCode v${FRAMEWORK_VERSION} %c Powered by the Rhythm of the Continent `,
17
+ 'background: #1EB53A; color: white; padding: 4px; border-radius: 4px 0 0 4px; font-weight: bold;',
18
+ 'background: #121212; color: #FCD116; padding: 4px; border-radius: 0 4px 4px 0;'
19
+ );
20
+
21
+ // 2. DOM Injection (Watermark)
22
+ // Ensures attribution exists in the DOM, invisible but present
23
+ if (typeof document !== 'undefined') {
24
+ const meta = document.createElement('meta');
25
+ meta.name = 'generator';
26
+ meta.content = `AfriCode ${FRAMEWORK_VERSION}`;
27
+ document.head.appendChild(meta);
28
+
29
+ // "Unremovable" attribute watcher
30
+ // If someone tries to remove the attribute, it puts it back
31
+ document.documentElement.setAttribute('data-africode', 'true');
32
+
33
+ const observer = new MutationObserver((mutations) => {
34
+ mutations.forEach((mutation) => {
35
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-africode') {
36
+ if (!document.documentElement.hasAttribute('data-africode')) {
37
+ document.documentElement.setAttribute('data-africode', 'true');
38
+ console.warn('AfriCode integrity check: Attribute restored.');
39
+ }
40
+ }
41
+ });
42
+ });
43
+
44
+ observer.observe(document.documentElement, { attributes: true });
45
+ }
46
+
47
+ // 3. Usage Telemetry (Mock)
48
+ // In a real scenario, this would POST to a server
49
+ const usage = {
50
+ url: typeof window !== 'undefined' ? window.location.href : 'server',
51
+ timestamp: Date.now(),
52
+ version: FRAMEWORK_VERSION
53
+ };
54
+
55
+ // Storing locally for now
56
+ try {
57
+ localStorage.setItem(HEARTBEAT_KEY, JSON.stringify(usage));
58
+ } catch (e) {
59
+ // Ignore in non-browser envs
60
+ }
61
+ }
62
+
63
+ // Auto-init on import
64
+ initHeartbeat();