@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
@@ -0,0 +1,206 @@
1
+ /**
2
+ * AfriCode CSRF Protection Middleware
3
+ *
4
+ * Double-submit cookie pattern for CSRF protection.
5
+ * Framework-level middleware — secure by default.
6
+ *
7
+ * How it works:
8
+ * 1. Server generates a random CSRF token and sets it as a cookie
9
+ * 2. Client reads the cookie and sends the token in a header (X-CSRF-Token)
10
+ * 3. Server compares cookie token with header token
11
+ * 4. If they match → request is from the legitimate client
12
+ * 5. If they don't match → request is cross-site (blocked)
13
+ *
14
+ * Why double-submit cookie:
15
+ * - No server-side session storage needed for the token
16
+ * - Works with stateless APIs
17
+ * - Compatible with single-page apps and server-rendered pages
18
+ *
19
+ * @module core/server/csrf
20
+ */
21
+
22
+ import { CsrfError } from '../errors.js';
23
+ import { emitSecurityViolation } from '../config.js';
24
+
25
+ /**
26
+ * Generate a cryptographically secure CSRF token
27
+ * @returns {string} 32-byte hex token
28
+ */
29
+ export function generateCsrfToken() {
30
+ const bytes = new Uint8Array(32);
31
+ crypto.getRandomValues(bytes);
32
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
33
+ }
34
+
35
+ /**
36
+ * Create the CSRF cookie header string
37
+ * @param {string} token - The CSRF token
38
+ * @param {Object} [opts] - Options
39
+ * @param {boolean} [opts.secure=false] - Set Secure flag (HTTPS only)
40
+ * @param {string} [opts.sameSite='Strict'] - SameSite policy
41
+ * @param {number} [opts.maxAge=86400] - Cookie max age in seconds (default: 24h)
42
+ * @returns {string} Set-Cookie header value
43
+ */
44
+ export function createCsrfCookie(token, opts = {}) {
45
+ const { secure = false, sameSite = 'Strict', maxAge = 86400 } = opts;
46
+ return `csrf_token=${token}; Path=/; Max-Age=${maxAge}; SameSite=${sameSite}${secure ? '; Secure' : ''}`;
47
+ }
48
+
49
+ /**
50
+ * Extract the CSRF token from the request cookie
51
+ * @param {Request} request
52
+ * @returns {string|null}
53
+ */
54
+ function getCsrfFromCookie(request) {
55
+ const cookieHeader = request.headers.get('Cookie');
56
+ if (!cookieHeader) {return null;}
57
+
58
+ const match = cookieHeader.match(/csrf_token=([^;]+)/);
59
+ return match ? match[1] : null;
60
+ }
61
+
62
+ /**
63
+ * Extract the CSRF token from the request header
64
+ * @param {Request} request
65
+ * @returns {string|null}
66
+ */
67
+ function getCsrfFromHeader(request) {
68
+ return request.headers.get('X-CSRF-Token') || null;
69
+ }
70
+
71
+ /**
72
+ * CSRF Protection Middleware
73
+ *
74
+ * Validates that the CSRF token in the cookie matches the one in the header.
75
+ * Only applies to state-changing methods (POST, PUT, PATCH, DELETE).
76
+ * GET, HEAD, OPTIONS are safe methods and are not checked.
77
+ *
78
+ * @param {Object} [config] - Configuration
79
+ * @param {string[]} [config.safeMethods=['GET', 'HEAD', 'OPTIONS']] - Methods that skip CSRF check
80
+ * @param {string[]} [config.excludePaths=[]] - Paths that skip CSRF check (e.g. public APIs)
81
+ * @param {string} [config.headerName='X-CSRF-Token'] - Header name for the token
82
+ * @param {string} [config.cookieName='csrf_token'] - Cookie name for the token
83
+ * @param {Function} [config.onFailure] - Custom failure handler (receives request, returns Response)
84
+ *
85
+ * @returns {Function} Middleware function: (request) => Response | null
86
+ *
87
+ * @example
88
+ * // In your server setup:
89
+ * const csrf = createCsrfMiddleware();
90
+ *
91
+ * // In your request handler:
92
+ * const csrfError = csrf(request);
93
+ * if (csrfError) return csrfError;
94
+ *
95
+ * @example
96
+ * // With configuration:
97
+ * const csrf = createCsrfMiddleware({
98
+ * excludePaths: ['/api/webhooks'],
99
+ * onFailure: (req) => new Response('Custom CSRF error', { status: 403 })
100
+ * });
101
+ */
102
+ export function createCsrfMiddleware(config = {}) {
103
+ const {
104
+ safeMethods = ['GET', 'HEAD', 'OPTIONS'],
105
+ excludePaths = [],
106
+ headerName = 'X-CSRF-Token',
107
+ cookieName = 'csrf_token',
108
+ onFailure = null
109
+ } = config;
110
+
111
+ /**
112
+ * @param {Request} request
113
+ * @returns {Response|null} Returns Response if CSRF fails, null if passes
114
+ */
115
+ return function csrfMiddleware(request) {
116
+ const method = request.method.toUpperCase();
117
+
118
+ // Safe methods don't need CSRF protection
119
+ if (safeMethods.includes(method)) {
120
+ return null;
121
+ }
122
+
123
+ // Check excluded paths
124
+ const url = new URL(request.url);
125
+ if (excludePaths.some(path => url.pathname.startsWith(path))) {
126
+ return null;
127
+ }
128
+
129
+ // Get tokens
130
+ const cookieToken = getCsrfFromCookie(request);
131
+ const headerToken = request.headers.get(headerName);
132
+
133
+ // Both must exist
134
+ if (!cookieToken || !headerToken) {
135
+ const error = new CsrfError('missing');
136
+ emitSecurityViolation(error);
137
+ if (onFailure) {return onFailure(request);}
138
+ return error.toResponse();
139
+ }
140
+
141
+ // Constant-time comparison to prevent timing attacks
142
+ if (!timingSafeEqual(cookieToken, headerToken)) {
143
+ const error = new CsrfError('mismatch');
144
+ emitSecurityViolation(error);
145
+ if (onFailure) {return onFailure(request);}
146
+ return error.toResponse();
147
+ }
148
+
149
+ // CSRF check passed
150
+ return null;
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Constant-time string comparison to prevent timing attacks.
156
+ * Both strings must be the same length for timing safety.
157
+ *
158
+ * @param {string} a
159
+ * @param {string} b
160
+ * @returns {boolean}
161
+ */
162
+ function timingSafeEqual(a, b) {
163
+ if (a.length !== b.length) {return false;}
164
+
165
+ const encoder = new TextEncoder();
166
+ const bufA = encoder.encode(a);
167
+ const bufB = encoder.encode(b);
168
+
169
+ // Use constant-time XOR comparison
170
+ let result = 0;
171
+ for (let i = 0; i < bufA.length; i++) {
172
+ result |= bufA[i] ^ bufB[i];
173
+ }
174
+
175
+ return result === 0;
176
+ }
177
+
178
+ /**
179
+ * Helper to inject CSRF token into a page response.
180
+ * Sets the cookie and adds a meta tag for client-side access.
181
+ *
182
+ * @param {string} html - The HTML response body
183
+ * @param {string} token - The CSRF token
184
+ * @returns {{ html: string, cookie: string }} Modified HTML and cookie header
185
+ *
186
+ * @example
187
+ * const token = generateCsrfToken();
188
+ * const { html, cookie } = injectCsrfToken(pageHtml, token);
189
+ * return new Response(html, {
190
+ * headers: {
191
+ * 'Content-Type': 'text/html',
192
+ * 'Set-Cookie': cookie
193
+ * }
194
+ * });
195
+ */
196
+ export function injectCsrfToken(html, token) {
197
+ const metaTag = `<meta name="csrf-token" content="${token}">`;
198
+ const modifiedHtml = html.replace('</head>', ` ${metaTag}\n</head>`);
199
+
200
+ return {
201
+ html: modifiedHtml,
202
+ cookie: createCsrfCookie(token)
203
+ };
204
+ }
205
+
206
+ export default { generateCsrfToken, createCsrfCookie, createCsrfMiddleware, injectCsrfToken };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * AfriCode Database Layer
3
+ * Zero-config SQLite integration powered by Bun
4
+ */
5
+
6
+ import { Database } from "bun:sqlite";
7
+
8
+ // Auto-create db file
9
+ const db = new Database("africode.sqlite", { create: true });
10
+
11
+ // Initialize Schema
12
+ db.run(`
13
+ CREATE TABLE IF NOT EXISTS users (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ email TEXT UNIQUE,
16
+ password_hash TEXT,
17
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
18
+ );
19
+ `);
20
+
21
+ db.run(`
22
+ CREATE TABLE IF NOT EXISTS sessions (
23
+ id TEXT PRIMARY KEY,
24
+ user_id INTEGER,
25
+ expires_at DATETIME,
26
+ FOREIGN KEY(user_id) REFERENCES users(id)
27
+ );
28
+ `);
29
+
30
+ export default db;
31
+
32
+ // Helper query function
33
+ export const query = {
34
+ getUserByEmail: (email) => db.query("SELECT * FROM users WHERE email = ?").get(email),
35
+ createUser: (email, hash) => db.run("INSERT INTO users (email, password_hash) VALUES (?, ?)", [email, hash]),
36
+ createSession: (sid, uid, exp) => db.run("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)", [sid, uid, exp]),
37
+ getSession: (sid) => db.query("SELECT * FROM sessions WHERE id = ?").get(sid),
38
+ deleteSession: (sid) => db.run("DELETE FROM sessions WHERE id = ?", [sid])
39
+ };
@@ -0,0 +1,324 @@
1
+ /**
2
+ * AfriCode Middleware Pipeline
3
+ *
4
+ * Chainable middleware system for cross-cutting concerns:
5
+ * - CORS handling
6
+ * - Request logging
7
+ * - Authentication checks
8
+ * - Rate limiting
9
+ * - Error handling
10
+ *
11
+ * Usage:
12
+ * ```javascript
13
+ * const app = new MiddlewareApp();
14
+ * app.use(cors({ origin: 'https://example.com' }));
15
+ * app.use(logging());
16
+ * app.use(authRequired());
17
+ * app.listen(3000);
18
+ * ```
19
+ */
20
+
21
+ export class MiddlewareApp {
22
+ constructor() {
23
+ this.middlewares = [];
24
+ this.errorHandlers = [];
25
+ }
26
+
27
+ /**
28
+ * Register a middleware function
29
+ * Middleware receives (request, response, next) and can:
30
+ * - Modify request/response
31
+ * - Call next() to continue
32
+ * - Return a response to short-circuit
33
+ */
34
+ use(middleware) {
35
+ if (typeof middleware !== 'function') {
36
+ throw new Error('Middleware must be a function');
37
+ }
38
+ this.middlewares.push(middleware);
39
+ return this; // Enable chaining
40
+ }
41
+
42
+ /**
43
+ * Register error handling middleware
44
+ */
45
+ onError(handler) {
46
+ if (typeof handler !== 'function') {
47
+ throw new Error('Error handler must be a function');
48
+ }
49
+ this.errorHandlers.push(handler);
50
+ return this;
51
+ }
52
+
53
+ /**
54
+ * Execute middleware chain
55
+ */
56
+ async executeMiddlewares(request, response) {
57
+ let index = 0;
58
+
59
+ const next = async () => {
60
+ if (index >= this.middlewares.length) {
61
+ return; // End of chain
62
+ }
63
+
64
+ const middleware = this.middlewares[index++];
65
+ try {
66
+ await middleware(request, response, next);
67
+ } catch (error) {
68
+ // Call error handlers
69
+ for (const handler of this.errorHandlers) {
70
+ await handler(error, request, response);
71
+ }
72
+ }
73
+ };
74
+
75
+ await next();
76
+ }
77
+
78
+ /**
79
+ * Start HTTP server
80
+ */
81
+ listen(port = 3000, callback) {
82
+ const server = Bun.serve({
83
+ port,
84
+ fetch: async (request) => {
85
+ const response = new Response();
86
+ await this.executeMiddlewares(request, response);
87
+ return response;
88
+ }
89
+ });
90
+
91
+ console.log(`[Middleware] Server listening on port ${port}`);
92
+ if (callback) callback();
93
+ return server;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Built-in Middleware Functions
99
+ */
100
+
101
+ /**
102
+ * CORS Middleware
103
+ * Handles Cross-Origin Resource Sharing
104
+ */
105
+ export function cors(options = {}) {
106
+ const {
107
+ origin = '*',
108
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
109
+ allowedHeaders = ['Content-Type', 'Authorization'],
110
+ credentials = false,
111
+ maxAge = 3600
112
+ } = options;
113
+
114
+ return async (request, response, next) => {
115
+ const requestOrigin = request.headers.get('origin');
116
+
117
+ // Check if origin is allowed
118
+ let allowOrigin = false;
119
+ if (origin === '*') {
120
+ allowOrigin = true;
121
+ } else if (Array.isArray(origin)) {
122
+ allowOrigin = origin.includes(requestOrigin);
123
+ } else if (typeof origin === 'function') {
124
+ allowOrigin = origin(requestOrigin);
125
+ } else {
126
+ allowOrigin = origin === requestOrigin;
127
+ }
128
+
129
+ if (allowOrigin) {
130
+ response.headers.set('Access-Control-Allow-Origin', requestOrigin || '*');
131
+ response.headers.set('Access-Control-Allow-Methods', methods.join(', '));
132
+ response.headers.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
133
+ response.headers.set('Access-Control-Max-Age', maxAge.toString());
134
+
135
+ if (credentials) {
136
+ response.headers.set('Access-Control-Allow-Credentials', 'true');
137
+ }
138
+ }
139
+
140
+ // Handle preflight requests
141
+ if (request.method === 'OPTIONS') {
142
+ return new Response(null, { status: 204 });
143
+ }
144
+
145
+ await next();
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Request Logging Middleware
151
+ */
152
+ export function logging(options = {}) {
153
+ const {
154
+ format = 'combined',
155
+ logFn = console.log
156
+ } = options;
157
+
158
+ return async (request, response, next) => {
159
+ const startTime = Date.now();
160
+ const method = request.method;
161
+ const url = new URL(request.url);
162
+ const pathname = url.pathname;
163
+
164
+ await next();
165
+
166
+ const duration = Date.now() - startTime;
167
+ const ip = request.headers.get('x-forwarded-for') || '127.0.0.1';
168
+
169
+ if (format === 'json') {
170
+ logFn(JSON.stringify({
171
+ ip,
172
+ method,
173
+ pathname,
174
+ duration,
175
+ timestamp: new Date().toISOString()
176
+ }));
177
+ } else if (format === 'combined') {
178
+ logFn(`[${new Date().toISOString()}] ${ip} - ${method} ${pathname} - ${duration}ms`);
179
+ } else {
180
+ logFn(`[${new Date().toISOString()}] ${method} ${pathname} - ${duration}ms`);
181
+ }
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Authentication Required Middleware
187
+ */
188
+ export function authRequired() {
189
+ return async (request, response, next) => {
190
+ // Extract session from cookies
191
+ const cookieHeader = request.headers.get('Cookie');
192
+ if (!cookieHeader) {
193
+ return new Response(
194
+ JSON.stringify({ error: 'Unauthorized' }),
195
+ { status: 401, headers: { 'Content-Type': 'application/json' } }
196
+ );
197
+ }
198
+
199
+ const sessionMatch = cookieHeader.match(/session_id=([^;]+)/);
200
+ if (!sessionMatch) {
201
+ return new Response(
202
+ JSON.stringify({ error: 'Unauthorized' }),
203
+ { status: 401, headers: { 'Content-Type': 'application/json' } }
204
+ );
205
+ }
206
+
207
+ // Attach session ID to request for later use
208
+ request.sessionId = sessionMatch[1];
209
+
210
+ await next();
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Rate Limiting Middleware
216
+ */
217
+ export function rateLimit(options = {}) {
218
+ const {
219
+ maxRequests = 100,
220
+ windowMs = 60000, // 1 minute
221
+ keyGenerator = (request) => request.headers.get('x-forwarded-for') || '127.0.0.1'
222
+ } = options;
223
+
224
+ const store = new Map();
225
+
226
+ return async (request, response, next) => {
227
+ const key = keyGenerator(request);
228
+ const now = Date.now();
229
+ const record = store.get(key);
230
+
231
+ // Clean up old records
232
+ if (record && now - record.resetTime > windowMs) {
233
+ store.delete(key);
234
+ }
235
+
236
+ // Check if limit exceeded
237
+ const current = store.get(key) || { count: 0, resetTime: now };
238
+ if (current.count >= maxRequests) {
239
+ return new Response(
240
+ JSON.stringify({ error: 'Rate limit exceeded' }),
241
+ { status: 429, headers: { 'Content-Type': 'application/json' } }
242
+ );
243
+ }
244
+
245
+ current.count++;
246
+ store.set(key, current);
247
+
248
+ // Add rate limit headers
249
+ response.headers.set('X-RateLimit-Limit', maxRequests.toString());
250
+ response.headers.set('X-RateLimit-Remaining', (maxRequests - current.count).toString());
251
+ response.headers.set('X-RateLimit-Reset', (current.resetTime + windowMs).toString());
252
+
253
+ await next();
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Error Handling Middleware
259
+ */
260
+ export function errorHandler() {
261
+ return async (error, request, response) => {
262
+ console.error('[Error]', error);
263
+
264
+ const statusCode = error.status || 500;
265
+ const message = error.message || 'Internal Server Error';
266
+
267
+ response.status = statusCode;
268
+ response.headers.set('Content-Type', 'application/json');
269
+
270
+ return new Response(
271
+ JSON.stringify({
272
+ error: message,
273
+ status: statusCode,
274
+ timestamp: new Date().toISOString()
275
+ }),
276
+ { status: statusCode, headers: { 'Content-Type': 'application/json' } }
277
+ );
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Content Type Validation Middleware
283
+ */
284
+ export function validateContentType(acceptedTypes = ['application/json']) {
285
+ return async (request, response, next) => {
286
+ if (request.method === 'POST' || request.method === 'PUT') {
287
+ const contentType = request.headers.get('Content-Type')?.split(';')[0];
288
+ if (!acceptedTypes.includes(contentType)) {
289
+ return new Response(
290
+ JSON.stringify({ error: 'Unsupported Content-Type' }),
291
+ { status: 415, headers: { 'Content-Type': 'application/json' } }
292
+ );
293
+ }
294
+ }
295
+ await next();
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Security Headers Middleware
301
+ */
302
+ export function securityHeaders() {
303
+ return async (request, response, next) => {
304
+ response.headers.set('X-Content-Type-Options', 'nosniff');
305
+ response.headers.set('X-Frame-Options', 'DENY');
306
+ response.headers.set('X-XSS-Protection', '1; mode=block');
307
+ response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
308
+ response.headers.set('Content-Security-Policy', "default-src 'self'");
309
+ response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
310
+
311
+ await next();
312
+ };
313
+ }
314
+
315
+ export default {
316
+ MiddlewareApp,
317
+ cors,
318
+ logging,
319
+ authRequired,
320
+ rateLimit,
321
+ errorHandler,
322
+ validateContentType,
323
+ securityHeaders
324
+ };