@flexireact/core 1.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.
@@ -0,0 +1,393 @@
1
+ /**
2
+ * FlexiReact Middleware System
3
+ *
4
+ * Middlewares run before every request and can:
5
+ * - Modify the request/response
6
+ * - Redirect or rewrite URLs
7
+ * - Add headers
8
+ * - Authenticate users
9
+ * - Log requests
10
+ *
11
+ * Usage:
12
+ * Create a middleware.js file in your project root:
13
+ *
14
+ * export default function middleware(request) {
15
+ * // Return a response to short-circuit
16
+ * // Return NextResponse.next() to continue
17
+ * // Return NextResponse.redirect() to redirect
18
+ * }
19
+ *
20
+ * export const config = {
21
+ * matcher: ['/protected/:path*']
22
+ * };
23
+ */
24
+
25
+ import fs from 'fs';
26
+ import path from 'path';
27
+ import { pathToFileURL } from 'url';
28
+
29
+ /**
30
+ * Middleware response helpers
31
+ */
32
+ export class MiddlewareResponse {
33
+ constructor(options = {}) {
34
+ this.type = options.type || 'next';
35
+ this.status = options.status || 200;
36
+ this.headers = new Map(Object.entries(options.headers || {}));
37
+ this.body = options.body || null;
38
+ this.url = options.url || null;
39
+ }
40
+
41
+ /**
42
+ * Continue to the next middleware/handler
43
+ */
44
+ static next(options = {}) {
45
+ return new MiddlewareResponse({ ...options, type: 'next' });
46
+ }
47
+
48
+ /**
49
+ * Redirect to a different URL
50
+ */
51
+ static redirect(url, status = 302) {
52
+ return new MiddlewareResponse({
53
+ type: 'redirect',
54
+ url,
55
+ status
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Rewrite the request to a different URL (internal)
61
+ */
62
+ static rewrite(url) {
63
+ return new MiddlewareResponse({
64
+ type: 'rewrite',
65
+ url
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Return a JSON response
71
+ */
72
+ static json(data, options = {}) {
73
+ return new MiddlewareResponse({
74
+ type: 'response',
75
+ status: options.status || 200,
76
+ headers: { 'Content-Type': 'application/json', ...options.headers },
77
+ body: JSON.stringify(data)
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Return an HTML response
83
+ */
84
+ static html(content, options = {}) {
85
+ return new MiddlewareResponse({
86
+ type: 'response',
87
+ status: options.status || 200,
88
+ headers: { 'Content-Type': 'text/html', ...options.headers },
89
+ body: content
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Middleware request wrapper
96
+ */
97
+ export class MiddlewareRequest {
98
+ constructor(req) {
99
+ this.raw = req;
100
+ this.method = req.method;
101
+ this.url = req.url;
102
+ this.headers = new Map(Object.entries(req.headers || {}));
103
+
104
+ // Parse URL
105
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
106
+ this.pathname = parsedUrl.pathname;
107
+ this.searchParams = parsedUrl.searchParams;
108
+ this.query = Object.fromEntries(parsedUrl.searchParams);
109
+
110
+ // Parse cookies
111
+ this.cookies = this._parseCookies(req.headers.cookie || '');
112
+ }
113
+
114
+ _parseCookies(cookieHeader) {
115
+ const cookies = new Map();
116
+ if (!cookieHeader) return cookies;
117
+
118
+ cookieHeader.split(';').forEach(cookie => {
119
+ const [name, ...rest] = cookie.split('=');
120
+ if (name) {
121
+ cookies.set(name.trim(), rest.join('=').trim());
122
+ }
123
+ });
124
+
125
+ return cookies;
126
+ }
127
+
128
+ /**
129
+ * Get a header value
130
+ */
131
+ header(name) {
132
+ return this.headers.get(name.toLowerCase());
133
+ }
134
+
135
+ /**
136
+ * Get a cookie value
137
+ */
138
+ cookie(name) {
139
+ return this.cookies.get(name);
140
+ }
141
+
142
+ /**
143
+ * Check if request matches a path pattern
144
+ */
145
+ matches(pattern) {
146
+ return matchPath(this.pathname, pattern);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Loads middleware from project
152
+ */
153
+ export async function loadMiddleware(projectRoot) {
154
+ const middlewarePath = path.join(projectRoot, 'middleware.js');
155
+
156
+ if (!fs.existsSync(middlewarePath)) {
157
+ return null;
158
+ }
159
+
160
+ try {
161
+ const url = pathToFileURL(middlewarePath).href;
162
+ const module = await import(`${url}?t=${Date.now()}`);
163
+
164
+ return {
165
+ handler: module.default,
166
+ config: module.config || {}
167
+ };
168
+ } catch (error) {
169
+ console.error('Failed to load middleware:', error);
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Runs middleware chain
176
+ */
177
+ export async function runMiddleware(req, res, middleware) {
178
+ if (!middleware) {
179
+ return { continue: true };
180
+ }
181
+
182
+ const { handler, config } = middleware;
183
+ const request = new MiddlewareRequest(req);
184
+
185
+ // Check if request matches middleware patterns
186
+ if (config.matcher) {
187
+ const patterns = Array.isArray(config.matcher) ? config.matcher : [config.matcher];
188
+ const matches = patterns.some(pattern => matchPath(request.pathname, pattern));
189
+
190
+ if (!matches) {
191
+ return { continue: true };
192
+ }
193
+ }
194
+
195
+ try {
196
+ const response = await handler(request);
197
+
198
+ if (!response || response.type === 'next') {
199
+ // Apply any headers from middleware
200
+ if (response?.headers) {
201
+ for (const [key, value] of response.headers) {
202
+ res.setHeader(key, value);
203
+ }
204
+ }
205
+ return { continue: true };
206
+ }
207
+
208
+ if (response.type === 'redirect') {
209
+ res.writeHead(response.status, { Location: response.url });
210
+ res.end();
211
+ return { continue: false };
212
+ }
213
+
214
+ if (response.type === 'rewrite') {
215
+ // Modify the request URL internally
216
+ req.url = response.url;
217
+ return { continue: true, rewritten: true };
218
+ }
219
+
220
+ if (response.type === 'response') {
221
+ // Apply headers
222
+ for (const [key, value] of response.headers) {
223
+ res.setHeader(key, value);
224
+ }
225
+ res.writeHead(response.status);
226
+ res.end(response.body);
227
+ return { continue: false };
228
+ }
229
+
230
+ return { continue: true };
231
+
232
+ } catch (error) {
233
+ console.error('Middleware error:', error);
234
+ return { continue: true, error };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Matches a path against a pattern
240
+ */
241
+ function matchPath(pathname, pattern) {
242
+ // Convert pattern to regex
243
+ let regex = pattern
244
+ .replace(/\*/g, '.*')
245
+ .replace(/:path\*/g, '.*')
246
+ .replace(/:(\w+)/g, '[^/]+');
247
+
248
+ regex = `^${regex}$`;
249
+
250
+ return new RegExp(regex).test(pathname);
251
+ }
252
+
253
+ /**
254
+ * Compose multiple middleware functions
255
+ */
256
+ export function composeMiddleware(...middlewares) {
257
+ return async (request) => {
258
+ for (const middleware of middlewares) {
259
+ const response = await middleware(request);
260
+
261
+ if (response && response.type !== 'next') {
262
+ return response;
263
+ }
264
+ }
265
+
266
+ return MiddlewareResponse.next();
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Built-in middleware helpers
272
+ */
273
+ export const middlewares = {
274
+ /**
275
+ * CORS middleware
276
+ */
277
+ cors(options = {}) {
278
+ const {
279
+ origin = '*',
280
+ methods = 'GET,HEAD,PUT,PATCH,POST,DELETE',
281
+ headers = 'Content-Type,Authorization',
282
+ credentials = false
283
+ } = options;
284
+
285
+ return (request) => {
286
+ const response = MiddlewareResponse.next({
287
+ headers: {
288
+ 'Access-Control-Allow-Origin': origin,
289
+ 'Access-Control-Allow-Methods': methods,
290
+ 'Access-Control-Allow-Headers': headers,
291
+ ...(credentials && { 'Access-Control-Allow-Credentials': 'true' })
292
+ }
293
+ });
294
+
295
+ // Handle preflight
296
+ if (request.method === 'OPTIONS') {
297
+ return MiddlewareResponse.json({}, { status: 204, headers: response.headers });
298
+ }
299
+
300
+ return response;
301
+ };
302
+ },
303
+
304
+ /**
305
+ * Basic auth middleware
306
+ */
307
+ basicAuth(options) {
308
+ const { username, password, realm = 'Protected' } = options;
309
+ const expected = Buffer.from(`${username}:${password}`).toString('base64');
310
+
311
+ return (request) => {
312
+ const auth = request.header('authorization');
313
+
314
+ if (!auth || !auth.startsWith('Basic ')) {
315
+ return MiddlewareResponse.html('Unauthorized', {
316
+ status: 401,
317
+ headers: { 'WWW-Authenticate': `Basic realm="${realm}"` }
318
+ });
319
+ }
320
+
321
+ const provided = auth.slice(6);
322
+ if (provided !== expected) {
323
+ return MiddlewareResponse.html('Unauthorized', { status: 401 });
324
+ }
325
+
326
+ return MiddlewareResponse.next();
327
+ };
328
+ },
329
+
330
+ /**
331
+ * Rate limiting middleware
332
+ */
333
+ rateLimit(options = {}) {
334
+ const { windowMs = 60000, max = 100 } = options;
335
+ const requests = new Map();
336
+
337
+ return (request) => {
338
+ const ip = request.header('x-forwarded-for') || 'unknown';
339
+ const now = Date.now();
340
+
341
+ // Clean old entries
342
+ for (const [key, data] of requests) {
343
+ if (now - data.start > windowMs) {
344
+ requests.delete(key);
345
+ }
346
+ }
347
+
348
+ // Check rate limit
349
+ const data = requests.get(ip) || { count: 0, start: now };
350
+ data.count++;
351
+ requests.set(ip, data);
352
+
353
+ if (data.count > max) {
354
+ return MiddlewareResponse.json(
355
+ { error: 'Too many requests' },
356
+ { status: 429 }
357
+ );
358
+ }
359
+
360
+ return MiddlewareResponse.next({
361
+ headers: {
362
+ 'X-RateLimit-Limit': String(max),
363
+ 'X-RateLimit-Remaining': String(max - data.count)
364
+ }
365
+ });
366
+ };
367
+ },
368
+
369
+ /**
370
+ * Logging middleware
371
+ */
372
+ logger(options = {}) {
373
+ const { format = 'combined' } = options;
374
+
375
+ return (request) => {
376
+ const start = Date.now();
377
+ const { method, pathname } = request;
378
+
379
+ console.log(`→ ${method} ${pathname}`);
380
+
381
+ return MiddlewareResponse.next();
382
+ };
383
+ }
384
+ };
385
+
386
+ export default {
387
+ MiddlewareRequest,
388
+ MiddlewareResponse,
389
+ loadMiddleware,
390
+ runMiddleware,
391
+ composeMiddleware,
392
+ middlewares
393
+ };