@coherent.js/api 1.0.0-beta.2

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/api/errors.d.ts +92 -0
  4. package/dist/api/errors.d.ts.map +1 -0
  5. package/dist/api/errors.js +161 -0
  6. package/dist/api/errors.js.map +1 -0
  7. package/dist/api/index.d.ts +61 -0
  8. package/dist/api/index.d.ts.map +1 -0
  9. package/dist/api/index.js +41 -0
  10. package/dist/api/index.js.map +1 -0
  11. package/dist/api/middleware.d.ts +57 -0
  12. package/dist/api/middleware.d.ts.map +1 -0
  13. package/dist/api/middleware.js +244 -0
  14. package/dist/api/middleware.js.map +1 -0
  15. package/dist/api/openapi.d.ts +54 -0
  16. package/dist/api/openapi.d.ts.map +1 -0
  17. package/dist/api/openapi.js +144 -0
  18. package/dist/api/openapi.js.map +1 -0
  19. package/dist/api/router.d.ts +368 -0
  20. package/dist/api/router.d.ts.map +1 -0
  21. package/dist/api/router.js +1508 -0
  22. package/dist/api/router.js.map +1 -0
  23. package/dist/api/security.d.ts +64 -0
  24. package/dist/api/security.d.ts.map +1 -0
  25. package/dist/api/security.js +239 -0
  26. package/dist/api/security.js.map +1 -0
  27. package/dist/api/serialization.d.ts +86 -0
  28. package/dist/api/serialization.d.ts.map +1 -0
  29. package/dist/api/serialization.js +151 -0
  30. package/dist/api/serialization.js.map +1 -0
  31. package/dist/api/validation.d.ts +34 -0
  32. package/dist/api/validation.d.ts.map +1 -0
  33. package/dist/api/validation.js +172 -0
  34. package/dist/api/validation.js.map +1 -0
  35. package/dist/index.cjs +1776 -0
  36. package/dist/index.cjs.map +7 -0
  37. package/dist/index.js +1722 -0
  38. package/dist/index.js.map +7 -0
  39. package/package.json +46 -0
  40. package/types/index.d.ts +720 -0
package/dist/index.js ADDED
@@ -0,0 +1,1722 @@
1
+ // src/router.js
2
+ import { createHash, randomBytes } from "node:crypto";
3
+
4
+ // src/errors.js
5
+ var ApiError = class _ApiError extends Error {
6
+ /**
7
+ * Create an API _error
8
+ * @param {string} message - Error message
9
+ * @param {number} statusCode - HTTP status code
10
+ * @param {Object} details - Additional _error details
11
+ */
12
+ constructor(message, statusCode = 500, details = {}) {
13
+ super(message);
14
+ this.name = "ApiError";
15
+ this.statusCode = statusCode;
16
+ this.details = details;
17
+ if (Error.captureStackTrace) {
18
+ Error.captureStackTrace(this, _ApiError);
19
+ }
20
+ }
21
+ /**
22
+ * Convert _error to JSON-serializable object
23
+ * @returns {Object} Error object
24
+ */
25
+ toJSON() {
26
+ return {
27
+ _error: this.name,
28
+ message: this.message,
29
+ statusCode: this.statusCode,
30
+ details: this.details
31
+ };
32
+ }
33
+ };
34
+ var ValidationError = class extends ApiError {
35
+ /**
36
+ * Create a validation _error
37
+ * @param {Object} errors - Validation errors
38
+ * @param {string} message - Error message
39
+ */
40
+ constructor(errors, message = "Validation failed") {
41
+ super(message, 400, { errors });
42
+ this.name = "ValidationError";
43
+ }
44
+ };
45
+ var AuthenticationError = class extends ApiError {
46
+ /**
47
+ * Create an authentication _error
48
+ * @param {string} message - Error message
49
+ */
50
+ constructor(message = "Authentication required") {
51
+ super(message, 401);
52
+ this.name = "AuthenticationError";
53
+ }
54
+ };
55
+ var AuthorizationError = class extends ApiError {
56
+ /**
57
+ * Create an authorization _error
58
+ * @param {string} message - Error message
59
+ */
60
+ constructor(message = "Access denied") {
61
+ super(message, 403);
62
+ this.name = "AuthorizationError";
63
+ }
64
+ };
65
+ var NotFoundError = class extends ApiError {
66
+ /**
67
+ * Create a not found _error
68
+ * @param {string} message - Error message
69
+ */
70
+ constructor(message = "Resource not found") {
71
+ super(message, 404);
72
+ this.name = "NotFoundError";
73
+ }
74
+ };
75
+ var ConflictError = class extends ApiError {
76
+ /**
77
+ * Create a conflict _error
78
+ * @param {string} message - Error message
79
+ */
80
+ constructor(message = "Resource conflict") {
81
+ super(message, 409);
82
+ this.name = "ConflictError";
83
+ }
84
+ };
85
+ function withErrorHandling(handler) {
86
+ return async (req, res, next) => {
87
+ try {
88
+ return await handler(req, res, next);
89
+ } catch (_error) {
90
+ if (_error instanceof ApiError) {
91
+ throw _error;
92
+ }
93
+ throw new ApiError(_error.message || "Internal server _error", 500);
94
+ }
95
+ };
96
+ }
97
+ function createErrorHandler() {
98
+ return (_error, req, res, next) => {
99
+ console.error("API Error:", _error);
100
+ if (res.headersSent) {
101
+ return next(_error);
102
+ }
103
+ const response = {
104
+ _error: _error.name || "Error",
105
+ message: _error.message || "An _error occurred",
106
+ statusCode: _error.statusCode || 500
107
+ };
108
+ if (_error.details) {
109
+ response.details = _error.details;
110
+ }
111
+ if (true) {
112
+ response.stack = _error.stack;
113
+ }
114
+ res.status(response.statusCode).json(response);
115
+ };
116
+ }
117
+
118
+ // src/validation.js
119
+ function validateAgainstSchema(schema, data) {
120
+ const errors = [];
121
+ if (schema.type === "object") {
122
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
123
+ errors.push({
124
+ field: "",
125
+ message: `Expected object, got ${typeof data}`
126
+ });
127
+ return { valid: false, errors };
128
+ }
129
+ if (schema.required && Array.isArray(schema.required)) {
130
+ for (const field of schema.required) {
131
+ if (!(field in data)) {
132
+ errors.push({
133
+ field,
134
+ message: `Required field '${field}' is missing`
135
+ });
136
+ }
137
+ }
138
+ }
139
+ if (schema.properties) {
140
+ for (const [field, fieldSchema] of Object.entries(schema.properties)) {
141
+ if (field in data) {
142
+ const fieldValue = data[field];
143
+ const fieldErrors = validateField(fieldSchema, fieldValue, field);
144
+ errors.push(...fieldErrors);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return {
150
+ valid: errors.length === 0,
151
+ errors
152
+ };
153
+ }
154
+ function validateField(schema, value, fieldName) {
155
+ const errors = [];
156
+ if (schema.type) {
157
+ if (schema.type === "string" && typeof value !== "string") {
158
+ errors.push({
159
+ field: fieldName,
160
+ message: `Expected string, got ${typeof value}`
161
+ });
162
+ } else if (schema.type === "number" && typeof value !== "number") {
163
+ errors.push({
164
+ field: fieldName,
165
+ message: `Expected number, got ${typeof value}`
166
+ });
167
+ } else if (schema.type === "boolean" && typeof value !== "boolean") {
168
+ errors.push({
169
+ field: fieldName,
170
+ message: `Expected boolean, got ${typeof value}`
171
+ });
172
+ } else if (schema.type === "array" && !Array.isArray(value)) {
173
+ errors.push({
174
+ field: fieldName,
175
+ message: `Expected array, got ${typeof value}`
176
+ });
177
+ }
178
+ }
179
+ if (schema.type === "string") {
180
+ if (schema.minLength && value.length < schema.minLength) {
181
+ errors.push({
182
+ field: fieldName,
183
+ message: `String must be at least ${schema.minLength} characters`
184
+ });
185
+ }
186
+ if (schema.maxLength && value.length > schema.maxLength) {
187
+ errors.push({
188
+ field: fieldName,
189
+ message: `String must be at most ${schema.maxLength} characters`
190
+ });
191
+ }
192
+ if (schema.format === "email" && !/^[^@]+@[^@]+\.[^@]+$/.test(value)) {
193
+ errors.push({
194
+ field: fieldName,
195
+ message: "Invalid email format"
196
+ });
197
+ }
198
+ }
199
+ if (schema.type === "number") {
200
+ if (schema.minimum !== void 0 && value < schema.minimum) {
201
+ errors.push({
202
+ field: fieldName,
203
+ message: `Number must be at least ${schema.minimum}`
204
+ });
205
+ }
206
+ if (schema.maximum !== void 0 && value > schema.maximum) {
207
+ errors.push({
208
+ field: fieldName,
209
+ message: `Number must be at most ${schema.maximum}`
210
+ });
211
+ }
212
+ }
213
+ return errors;
214
+ }
215
+ function withValidation(schema) {
216
+ return (req, res, next) => {
217
+ const data = req.body || {};
218
+ const result = validateAgainstSchema(schema, data);
219
+ if (!result.valid) {
220
+ throw new ValidationError(result.errors);
221
+ }
222
+ next();
223
+ };
224
+ }
225
+ function withQueryValidation(schema) {
226
+ return (req, res, next) => {
227
+ const data = req.query || {};
228
+ const result = validateAgainstSchema(schema, data);
229
+ if (!result.valid) {
230
+ throw new ValidationError(result.errors);
231
+ }
232
+ next();
233
+ };
234
+ }
235
+ function withParamsValidation(schema) {
236
+ return (req, res, next) => {
237
+ const data = req.params || {};
238
+ const result = validateAgainstSchema(schema, data);
239
+ if (!result.valid) {
240
+ throw new ValidationError(result.errors);
241
+ }
242
+ next();
243
+ };
244
+ }
245
+
246
+ // src/router.js
247
+ import { createServer } from "node:http";
248
+ import { parse as parseUrl } from "node:url";
249
+ var HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
250
+ function parseBody(req, maxSize = 1024 * 1024) {
251
+ return new Promise((resolve, reject) => {
252
+ if (req.method === "GET" || req.method === "DELETE") {
253
+ resolve({});
254
+ return;
255
+ }
256
+ let body = "";
257
+ let size = 0;
258
+ req.on("data", (chunk) => {
259
+ size += chunk.length;
260
+ if (size > maxSize) {
261
+ reject(new Error("Request body too large"));
262
+ return;
263
+ }
264
+ body += chunk.toString();
265
+ });
266
+ req.on("end", () => {
267
+ try {
268
+ const contentType = req.headers["content-type"] || "";
269
+ if (contentType.includes("application/json")) {
270
+ const parsed = body ? JSON.parse(body) : {};
271
+ resolve(sanitizeInput(parsed));
272
+ } else {
273
+ resolve({});
274
+ }
275
+ } catch {
276
+ reject(new Error("Invalid JSON body"));
277
+ }
278
+ });
279
+ req.on("_error", reject);
280
+ });
281
+ }
282
+ function sanitizeInput(obj) {
283
+ if (typeof obj !== "object" || obj === null) return obj;
284
+ const sanitized = {};
285
+ for (const [key, value] of Object.entries(obj)) {
286
+ if (key.startsWith("__") || key.includes("prototype")) continue;
287
+ if (typeof value === "string") {
288
+ sanitized[key] = value.replace(/<script[^>]*>.*?<\/script>/gi, "").replace(/javascript:/gi, "").replace(/on\w+=/gi, "");
289
+ } else if (typeof value === "object") {
290
+ sanitized[key] = sanitizeInput(value);
291
+ } else {
292
+ sanitized[key] = value;
293
+ }
294
+ }
295
+ return sanitized;
296
+ }
297
+ var rateLimitStore = /* @__PURE__ */ new Map();
298
+ function checkRateLimit(ip, windowMs = 6e4, maxRequests = 100) {
299
+ const now = Date.now();
300
+ const key = ip;
301
+ if (!rateLimitStore.has(key)) {
302
+ rateLimitStore.set(key, { count: 1, resetTime: now + windowMs });
303
+ return true;
304
+ }
305
+ const record = rateLimitStore.get(key);
306
+ if (now > record.resetTime) {
307
+ record.count = 1;
308
+ record.resetTime = now + windowMs;
309
+ return true;
310
+ }
311
+ if (record.count >= maxRequests) {
312
+ return false;
313
+ }
314
+ record.count++;
315
+ return true;
316
+ }
317
+ function addSecurityHeaders(res, corsOrigin = null) {
318
+ const origin = corsOrigin || "http://localhost:3000";
319
+ res.setHeader("Access-Control-Allow-Origin", origin);
320
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
321
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
322
+ res.setHeader("Access-Control-Allow-Credentials", "true");
323
+ res.setHeader("X-Content-Type-Options", "nosniff");
324
+ res.setHeader("X-Frame-Options", "DENY");
325
+ res.setHeader("X-XSS-Protection", "1; mode=block");
326
+ res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
327
+ res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
328
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
329
+ }
330
+ function extractParams(pattern, path) {
331
+ const patternParts = pattern.split("/");
332
+ const pathParts = path.split("/");
333
+ const params = {};
334
+ const hasMultiWildcard = patternParts.includes("**");
335
+ const hasSingleWildcard = patternParts.includes("*");
336
+ if (!hasMultiWildcard && !hasSingleWildcard && patternParts.length !== pathParts.length) {
337
+ return null;
338
+ }
339
+ for (let i = 0; i < patternParts.length; i++) {
340
+ const patternPart = patternParts[i];
341
+ const pathPart = pathParts[i];
342
+ if (patternPart.startsWith(":")) {
343
+ const match = patternPart.match(/^:([^(]+)(\(([^)]+)\))?(\?)?$/);
344
+ if (match) {
345
+ const [, paramName, , constraint, optional] = match;
346
+ if (optional && !pathPart) {
347
+ continue;
348
+ }
349
+ if (constraint) {
350
+ const regex = new RegExp(`^${constraint}$`);
351
+ if (!regex.test(pathPart)) {
352
+ return null;
353
+ }
354
+ }
355
+ params[paramName] = pathPart;
356
+ } else {
357
+ params[patternPart.slice(1)] = pathPart;
358
+ }
359
+ } else if (patternPart === "*") {
360
+ params.splat = pathPart;
361
+ } else if (patternPart === "**") {
362
+ params.splat = pathParts.slice(i).join("/");
363
+ return params;
364
+ } else if (patternPart !== pathPart) {
365
+ return null;
366
+ }
367
+ }
368
+ return params;
369
+ }
370
+ function processRoutes(routeObj, router, basePath = "") {
371
+ if (!routeObj || typeof routeObj !== "object") return;
372
+ Object.entries(routeObj).forEach(([key, config]) => {
373
+ if (!config || typeof config !== "object") return;
374
+ if (config.ws && typeof config.ws === "function") {
375
+ const cleanKey = key.startsWith("/") ? key.slice(1) : key;
376
+ const wsPath = basePath ? `${basePath}/${cleanKey}` : `/${cleanKey}`;
377
+ router.addWebSocketRoute(wsPath, config.ws);
378
+ return;
379
+ }
380
+ if (HTTP_METHODS.includes(key.toUpperCase())) {
381
+ registerRoute(key.toUpperCase(), config, router, basePath);
382
+ } else {
383
+ const cleanKey = key.startsWith("/") ? key.slice(1) : key;
384
+ const path = basePath ? `${basePath}/${cleanKey}` : `/${cleanKey}`;
385
+ processRoutes(config, router, path);
386
+ }
387
+ });
388
+ }
389
+ function registerRoute(method, config, router, path) {
390
+ const {
391
+ handler,
392
+ handlers,
393
+ validation,
394
+ middleware,
395
+ errorHandling = true,
396
+ path: customPath,
397
+ name
398
+ } = config;
399
+ const routePath = customPath || path || "/";
400
+ const chain = [];
401
+ if (middleware) {
402
+ chain.push(...Array.isArray(middleware) ? middleware : [middleware]);
403
+ }
404
+ if (validation) {
405
+ chain.push(withValidation(validation));
406
+ }
407
+ if (handlers) {
408
+ chain.push(...handlers);
409
+ } else if (handler) {
410
+ chain.push(handler);
411
+ } else {
412
+ console.warn(`No handler for ${method} ${routePath}`);
413
+ return;
414
+ }
415
+ if (errorHandling) {
416
+ chain.forEach((fn, i) => {
417
+ chain[i] = withErrorHandling(fn);
418
+ });
419
+ }
420
+ router.addRoute(method, routePath, async (req, res) => {
421
+ try {
422
+ let result = null;
423
+ for (const fn of chain) {
424
+ result = await fn(req, res);
425
+ if (result && typeof result === "object") {
426
+ break;
427
+ }
428
+ }
429
+ if (result && typeof result === "object") {
430
+ res.writeHead(200, { "Content-Type": "application/json" });
431
+ res.end(JSON.stringify(result));
432
+ } else {
433
+ res.writeHead(204);
434
+ res.end();
435
+ }
436
+ } catch (_error) {
437
+ const statusCode = _error.statusCode || 500;
438
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
439
+ res.end(JSON.stringify({ _error: _error.message }));
440
+ }
441
+ }, { name });
442
+ }
443
+ var SimpleRouter = class {
444
+ constructor(options = {}) {
445
+ this.routes = [];
446
+ this.routeCache = /* @__PURE__ */ new Map();
447
+ this.namedRoutes = /* @__PURE__ */ new Map();
448
+ this.maxCacheSize = options.maxCacheSize || 1e3;
449
+ this.routeGroups = [];
450
+ this.globalMiddleware = [];
451
+ this.enableCompilation = options.enableCompilation !== false;
452
+ this.compiledRoutes = /* @__PURE__ */ new Map();
453
+ this.routeCompilationCache = /* @__PURE__ */ new Map();
454
+ this.enableVersioning = options.enableVersioning || false;
455
+ this.defaultVersion = options.defaultVersion || "v1";
456
+ this.versionHeader = options.versionHeader || "api-version";
457
+ this.versionedRoutes = /* @__PURE__ */ new Map();
458
+ this.enableContentNegotiation = options.enableContentNegotiation !== false;
459
+ this.defaultContentType = options.defaultContentType || "application/json";
460
+ this.enableWebSockets = options.enableWebSockets || false;
461
+ this.wsRoutes = [];
462
+ this.wsConnections = /* @__PURE__ */ new Map();
463
+ this.enableMetrics = options.enableMetrics || false;
464
+ if (this.enableMetrics) {
465
+ this.metrics = {
466
+ requests: 0,
467
+ cacheHits: 0,
468
+ compilationHits: 0,
469
+ routeMatches: /* @__PURE__ */ new Map(),
470
+ responseTime: [],
471
+ errors: 0,
472
+ versionRequests: /* @__PURE__ */ new Map(),
473
+ // Track requests per version
474
+ contentTypeRequests: /* @__PURE__ */ new Map(),
475
+ // Track requests per content type
476
+ wsConnections: 0,
477
+ // Track WebSocket connections
478
+ wsMessages: 0
479
+ // Track WebSocket messages
480
+ };
481
+ }
482
+ }
483
+ /**
484
+ * Add an HTTP route to the router
485
+ *
486
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE, PATCH)
487
+ * @param {string} path - Route path pattern (supports :param and wildcards)
488
+ * @param {Function} handler - Route handler function
489
+ * @param {Object} [options={}] - Route options
490
+ * @param {Array} [options.middleware] - Route-specific middleware
491
+ * @param {string} [options.name] - Named route for URL generation
492
+ * @param {string} [options.version] - API version for this route
493
+ *
494
+ * @example
495
+ * router.addRoute('GET', '/users/:id', (req, res) => {
496
+ * return { user: { id: req.params.id } };
497
+ * }, { name: 'getUser', version: 'v2' });
498
+ */
499
+ addRoute(method, path, handler, options = {}) {
500
+ const prefix = this.getCurrentPrefix();
501
+ const fullPath = prefix + (path.startsWith("/") ? path : `/${path}`);
502
+ const groupMiddleware = this.getCurrentGroupMiddleware();
503
+ const routeMiddleware = options.middleware || [];
504
+ const allMiddleware = [...this.globalMiddleware, ...groupMiddleware, ...routeMiddleware];
505
+ const route = {
506
+ method: method.toUpperCase(),
507
+ path: fullPath,
508
+ handler,
509
+ middleware: allMiddleware,
510
+ name: options.name,
511
+ version: options.version || this.defaultVersion
512
+ };
513
+ if (this.enableCompilation) {
514
+ route.compiled = this.compileRoute(fullPath);
515
+ }
516
+ this.routes.push(route);
517
+ if (this.enableVersioning) {
518
+ if (!this.versionedRoutes.has(route.version)) {
519
+ this.versionedRoutes.set(route.version, []);
520
+ }
521
+ this.versionedRoutes.get(route.version).push(route);
522
+ }
523
+ if (options.name) {
524
+ this.namedRoutes.set(options.name, { method: route.method, path: fullPath, version: route.version });
525
+ }
526
+ }
527
+ /**
528
+ * Add a versioned route
529
+ * @param {string} version - API version (e.g., 'v1', 'v2')
530
+ * @param {string} method - HTTP method
531
+ * @param {string} path - Route path
532
+ * @param {Function} handler - Route handler
533
+ * @param {Object} options - Route options
534
+ */
535
+ addVersionedRoute(version, method, path, handler, options = {}) {
536
+ this.addRoute(method, path, handler, { ...options, version });
537
+ }
538
+ /**
539
+ * Add route with content negotiation support
540
+ * @param {string} method - HTTP method
541
+ * @param {string} path - Route path
542
+ * @param {Object} handlers - Content type handlers { 'application/json': handler, 'text/xml': handler }
543
+ * @param {Object} options - Route options
544
+ */
545
+ addContentNegotiatedRoute(method, path, handlers, options = {}) {
546
+ const negotiationHandler = async (req, res) => {
547
+ const acceptedType = this.negotiateContentType(req, Object.keys(handlers));
548
+ if (this.enableMetrics) {
549
+ this.metrics.contentTypeRequests.set(acceptedType, (this.metrics.contentTypeRequests.get(acceptedType) || 0) + 1);
550
+ }
551
+ const handler = handlers[acceptedType];
552
+ if (!handler) {
553
+ res.writeHead(406, { "Content-Type": "application/json" });
554
+ res.end(JSON.stringify({
555
+ _error: "Not Acceptable",
556
+ supportedTypes: Object.keys(handlers)
557
+ }));
558
+ return;
559
+ }
560
+ const result = await handler(req, res);
561
+ if (result && typeof result === "object") {
562
+ res.writeHead(200, { "Content-Type": acceptedType });
563
+ if (acceptedType === "application/json") {
564
+ res.end(JSON.stringify(result));
565
+ } else if (acceptedType === "text/xml" || acceptedType === "application/xml") {
566
+ res.end(this.objectToXml(result));
567
+ } else if (acceptedType === "text/html") {
568
+ res.end(typeof result === "string" ? result : `<pre>${JSON.stringify(result, null, 2)}</pre>`);
569
+ } else if (acceptedType === "text/plain") {
570
+ res.end(typeof result === "string" ? result : JSON.stringify(result));
571
+ } else {
572
+ res.end(JSON.stringify(result));
573
+ }
574
+ }
575
+ };
576
+ this.addRoute(method, path, negotiationHandler, options);
577
+ }
578
+ /**
579
+ * Negotiate content type based on Accept header
580
+ * @param {Object} req - Request object
581
+ * @param {Array} supportedTypes - Array of supported content types
582
+ * @returns {string} Best matching content type
583
+ * @private
584
+ */
585
+ negotiateContentType(req, supportedTypes) {
586
+ const acceptHeader = req.headers.accept || this.defaultContentType;
587
+ const acceptedTypes = acceptHeader.split(",").map((type) => {
588
+ const [mediaType, ...params] = type.trim().split(";");
589
+ const qValue = params.find((p) => p.trim().startsWith("q="));
590
+ const quality = qValue ? parseFloat(qValue.split("=")[1]) : 1;
591
+ return { type: mediaType.trim(), quality };
592
+ }).sort((a, b) => b.quality - a.quality);
593
+ for (const accepted of acceptedTypes) {
594
+ if (accepted.type === "*/*") {
595
+ return supportedTypes[0] || this.defaultContentType;
596
+ }
597
+ const [mainType, subType] = accepted.type.split("/");
598
+ for (const supported of supportedTypes) {
599
+ const [supportedMain, supportedSub] = supported.split("/");
600
+ if (accepted.type === supported || mainType === supportedMain && subType === "*" || mainType === "*" && subType === supportedSub) {
601
+ return supported;
602
+ }
603
+ }
604
+ }
605
+ return supportedTypes[0] || this.defaultContentType;
606
+ }
607
+ /**
608
+ * Convert object to XML string
609
+ * @param {Object} obj - Object to convert
610
+ * @param {string} rootName - Root element name
611
+ * @returns {string} XML string
612
+ * @private
613
+ */
614
+ objectToXml(obj, rootName = "root") {
615
+ const xmlEscape = (str) => String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
616
+ const toXml = (obj2, name) => {
617
+ if (obj2 === null || obj2 === void 0) {
618
+ return `<${name}/>`;
619
+ }
620
+ if (typeof obj2 !== "object") {
621
+ return `<${name}>${xmlEscape(obj2)}</${name}>`;
622
+ }
623
+ if (Array.isArray(obj2)) {
624
+ return obj2.map((item) => toXml(item, "item")).join("");
625
+ }
626
+ const content = Object.entries(obj2).map(([key, value]) => toXml(value, key)).join("");
627
+ return `<${name}>${content}</${name}>`;
628
+ };
629
+ return `<?xml version="1.0" encoding="UTF-8"?>${toXml(obj, rootName)}`;
630
+ }
631
+ /**
632
+ * Add WebSocket route
633
+ * @param {string} path - WebSocket path
634
+ * @param {Function} handler - WebSocket handler function
635
+ * @param {Object} options - Route options
636
+ */
637
+ addWebSocketRoute(path, handler, options = {}) {
638
+ if (!this.enableWebSockets) {
639
+ throw new Error("WebSocket routing is disabled. Enable with { enableWebSockets: true }");
640
+ }
641
+ const prefix = this.getCurrentPrefix();
642
+ const fullPath = prefix + (path.startsWith("/") ? path : `/${path}`);
643
+ const wsRoute = {
644
+ path: fullPath,
645
+ handler,
646
+ name: options.name,
647
+ version: options.version || this.defaultVersion,
648
+ compiled: this.enableCompilation ? this.compileRoute(fullPath) : null
649
+ };
650
+ this.wsRoutes.push(wsRoute);
651
+ if (options.name) {
652
+ this.namedRoutes.set(options.name, { method: "WS", path: fullPath, version: wsRoute.version });
653
+ }
654
+ }
655
+ /**
656
+ * Handle WebSocket upgrade request
657
+ * @param {Object} request - HTTP request object
658
+ * @param {Object} socket - Socket object
659
+ * @param {Buffer} head - First packet of the upgraded stream
660
+ */
661
+ handleWebSocketUpgrade(request, socket, head) {
662
+ if (!this.enableWebSockets) {
663
+ socket.end("HTTP/1.1 501 Not Implemented\r\n\r\n");
664
+ return;
665
+ }
666
+ const url = new URL(request.url, `http://${request.headers.host}`);
667
+ const pathname = url.pathname;
668
+ let matchedRoute = null;
669
+ for (const wsRoute of this.wsRoutes) {
670
+ let params = null;
671
+ if (this.enableCompilation && wsRoute.compiled) {
672
+ params = this.matchCompiledRoute(wsRoute.compiled, pathname);
673
+ } else {
674
+ params = extractParams(wsRoute.path, pathname);
675
+ }
676
+ if (params !== null) {
677
+ matchedRoute = { route: wsRoute, params };
678
+ break;
679
+ }
680
+ }
681
+ if (!matchedRoute) {
682
+ socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
683
+ return;
684
+ }
685
+ this.createWebSocketConnection(request, socket, head, matchedRoute);
686
+ }
687
+ /**
688
+ * Create WebSocket connection
689
+ * @param {Object} request - HTTP request object
690
+ * @param {Object} socket - Socket object
691
+ * @param {Buffer} head - First packet of the upgraded stream
692
+ * @param {Object} matchedRoute - Matched WebSocket route
693
+ * @private
694
+ */
695
+ createWebSocketConnection(request, socket, head, matchedRoute) {
696
+ const key = request.headers["sec-websocket-key"];
697
+ if (!key) {
698
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
699
+ return;
700
+ }
701
+ const acceptKey = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
702
+ const responseHeaders = [
703
+ "HTTP/1.1 101 Switching Protocols",
704
+ "Upgrade: websocket",
705
+ "Connection: Upgrade",
706
+ `Sec-WebSocket-Accept: ${acceptKey}`,
707
+ "",
708
+ ""
709
+ ].join("\r\n");
710
+ socket.write(responseHeaders);
711
+ const ws = this.createWebSocketWrapper(socket, matchedRoute);
712
+ const connectionId = randomBytes(16).toString("hex");
713
+ this.wsConnections.set(connectionId, ws);
714
+ if (this.enableMetrics) {
715
+ this.metrics.wsConnections++;
716
+ }
717
+ ws.id = connectionId;
718
+ ws.params = matchedRoute.params;
719
+ ws.path = matchedRoute.route.path;
720
+ socket.on("close", () => {
721
+ if (matchedRoute.route.handler.onClose) {
722
+ matchedRoute.route.handler.onClose(ws);
723
+ }
724
+ if (ws.onclose) {
725
+ try {
726
+ ws.onclose();
727
+ } catch (_error) {
728
+ console.error("WebSocket onclose handler _error:", _error);
729
+ }
730
+ }
731
+ this.wsConnections.delete(connectionId);
732
+ if (this.enableMetrics) {
733
+ this.metrics.wsConnections--;
734
+ }
735
+ });
736
+ try {
737
+ matchedRoute.route.handler(ws, request);
738
+ } catch (err) {
739
+ console.error("WebSocket upgrade _error:", err);
740
+ socket.end("HTTP/1.1 500 Internal Server Error\r\n\r\n");
741
+ }
742
+ }
743
+ /**
744
+ * Create WebSocket wrapper with message handling
745
+ * @param {Object} socket - Raw socket
746
+ * @param {Object} matchedRoute - Matched route info
747
+ * @returns {Object} WebSocket wrapper
748
+ * @private
749
+ */
750
+ createWebSocketWrapper(socket) {
751
+ const ws = {
752
+ socket,
753
+ readyState: 1,
754
+ // OPEN
755
+ send(data) {
756
+ if (this.readyState !== 1) return;
757
+ const message = typeof data === "string" ? data : JSON.stringify(data);
758
+ const frame = this.createFrame(message);
759
+ socket.write(frame);
760
+ if (ws.router && ws.router.enableMetrics) {
761
+ ws.router.metrics.wsMessages++;
762
+ }
763
+ },
764
+ close(code = 1e3, reason = "") {
765
+ if (this.readyState !== 1) return;
766
+ this.readyState = 3;
767
+ const frame = this.createCloseFrame(code, reason);
768
+ socket.write(frame);
769
+ socket.destroy();
770
+ },
771
+ ping(data = Buffer.alloc(0)) {
772
+ if (this.readyState !== 1) return;
773
+ const frame = this.createPingFrame(data);
774
+ socket.write(frame);
775
+ },
776
+ createFrame(data) {
777
+ const payload = Buffer.from(data, "utf8");
778
+ const payloadLength = payload.length;
779
+ let frame;
780
+ if (payloadLength < 126) {
781
+ frame = Buffer.allocUnsafe(2 + payloadLength);
782
+ frame[0] = 129;
783
+ frame[1] = payloadLength;
784
+ payload.copy(frame, 2);
785
+ } else if (payloadLength < 65536) {
786
+ frame = Buffer.allocUnsafe(4 + payloadLength);
787
+ frame[0] = 129;
788
+ frame[1] = 126;
789
+ frame.writeUInt16BE(payloadLength, 2);
790
+ payload.copy(frame, 4);
791
+ } else {
792
+ frame = Buffer.allocUnsafe(10 + payloadLength);
793
+ frame[0] = 129;
794
+ frame[1] = 127;
795
+ frame.writeUInt32BE(0, 2);
796
+ frame.writeUInt32BE(payloadLength, 6);
797
+ payload.copy(frame, 10);
798
+ }
799
+ return frame;
800
+ },
801
+ createCloseFrame(code, reason) {
802
+ const reasonBuffer = Buffer.from(reason, "utf8");
803
+ const frame = Buffer.allocUnsafe(4 + reasonBuffer.length);
804
+ frame[0] = 136;
805
+ frame[1] = 2 + reasonBuffer.length;
806
+ frame.writeUInt16BE(code, 2);
807
+ reasonBuffer.copy(frame, 4);
808
+ return frame;
809
+ },
810
+ createPingFrame(data) {
811
+ const frame = Buffer.allocUnsafe(2 + data.length);
812
+ frame[0] = 137;
813
+ frame[1] = data.length;
814
+ data.copy(frame, 2);
815
+ return frame;
816
+ }
817
+ };
818
+ ws.router = this;
819
+ socket.on("data", (buffer) => {
820
+ try {
821
+ const message = this.parseWebSocketFrame(buffer);
822
+ if (message && ws.onmessage) {
823
+ ws.onmessage({ data: message });
824
+ }
825
+ } catch {
826
+ }
827
+ });
828
+ socket.on("_error", (err) => {
829
+ console.log("WebSocket socket _error (connection likely closed):", err.code);
830
+ });
831
+ return ws;
832
+ }
833
+ /**
834
+ * Parse WebSocket frame
835
+ * @param {Buffer} buffer - Raw frame data
836
+ * @returns {string|null} Parsed message
837
+ * @private
838
+ */
839
+ parseWebSocketFrame(buffer) {
840
+ if (buffer.length < 2) return null;
841
+ const firstByte = buffer[0];
842
+ const secondByte = buffer[1];
843
+ const opcode = firstByte & 15;
844
+ const masked = (secondByte & 128) === 128;
845
+ let payloadLength = secondByte & 127;
846
+ if (opcode === 8) {
847
+ return null;
848
+ }
849
+ if (opcode !== 1) {
850
+ return null;
851
+ }
852
+ let offset = 2;
853
+ if (payloadLength === 126) {
854
+ if (buffer.length < offset + 2) return null;
855
+ payloadLength = buffer.readUInt16BE(offset);
856
+ offset += 2;
857
+ } else if (payloadLength === 127) {
858
+ if (buffer.length < offset + 8) return null;
859
+ payloadLength = buffer.readUInt32BE(offset + 4);
860
+ offset += 8;
861
+ }
862
+ if (masked) {
863
+ if (buffer.length < offset + 4 + payloadLength) return null;
864
+ const maskKey = buffer.slice(offset, offset + 4);
865
+ offset += 4;
866
+ const payload = buffer.slice(offset, offset + payloadLength);
867
+ for (let i = 0; i < payload.length; i++) {
868
+ payload[i] ^= maskKey[i % 4];
869
+ }
870
+ return payload.toString("utf8");
871
+ }
872
+ if (buffer.length < offset + payloadLength) return null;
873
+ return buffer.slice(offset, offset + payloadLength).toString("utf8");
874
+ }
875
+ /**
876
+ * Broadcast message to all WebSocket connections on a path
877
+ * @param {string} path - WebSocket path pattern
878
+ * @param {*} message - Message to broadcast
879
+ * @param {string} [excludeId=null] - Connection ID to exclude from broadcast
880
+ */
881
+ broadcast(path, message, excludeId = null) {
882
+ for (const [id, ws] of this.wsConnections) {
883
+ if (id === excludeId) continue;
884
+ if (ws.path === path || path === "*" && ws.path) {
885
+ try {
886
+ ws.send(message);
887
+ } catch {
888
+ console.log("Failed to send message to connection:", id);
889
+ }
890
+ }
891
+ }
892
+ }
893
+ /**
894
+ * Get active WebSocket connections
895
+ * @returns {Array} Array of connection info
896
+ */
897
+ getWebSocketConnections() {
898
+ return Array.from(this.wsConnections.entries()).map(([id, ws]) => ({
899
+ id,
900
+ path: ws.path,
901
+ params: ws.params,
902
+ readyState: ws.readyState
903
+ }));
904
+ }
905
+ /**
906
+ * Get version from request
907
+ * @param {Object} req - Request object
908
+ * @returns {string} API version
909
+ * @private
910
+ */
911
+ getRequestVersion(req) {
912
+ if (req.headers[this.versionHeader]) {
913
+ return req.headers[this.versionHeader];
914
+ }
915
+ const pathMatch = req.url.match(/^\/v(\d+)/);
916
+ if (pathMatch) {
917
+ return `v${pathMatch[1]}`;
918
+ }
919
+ if (req.query && req.query.version) {
920
+ return req.query.version;
921
+ }
922
+ return this.defaultVersion;
923
+ }
924
+ /**
925
+ * Generate URL for named route with parameter substitution
926
+ *
927
+ * @param {string} name - Route name (set during route registration)
928
+ * @param {Object} [params={}] - Parameters to substitute in the URL pattern
929
+ * @returns {string} Generated URL with parameters substituted
930
+ * @throws {Error} If named route is not found
931
+ *
932
+ * @example
933
+ * // Route registered as: router.addRoute('GET', '/users/:id', handler, { name: 'getUser' })
934
+ * const url = router.url('getUser', { id: 123 }); // '/users/123'
935
+ *
936
+ * // With constrained parameters
937
+ * const url = router.url('getUserPosts', { userId: 123, postId: 456 }); // '/users/123/posts/456'
938
+ */
939
+ generateUrl(name, params = {}) {
940
+ const route = this.namedRoutes.get(name);
941
+ if (!route) {
942
+ throw new Error(`Named route '${name}' not found`);
943
+ }
944
+ let url = route.path;
945
+ for (const [key, value] of Object.entries(params)) {
946
+ const paramPattern = new RegExp(`:${key}(\\([^)]+\\))?`, "g");
947
+ url = url.replace(paramPattern, encodeURIComponent(value));
948
+ }
949
+ return url;
950
+ }
951
+ /**
952
+ * Add routes from configuration object
953
+ *
954
+ * @param {Object} routeConfig - Route configuration object with nested structure
955
+ * @description Processes nested route objects and registers HTTP and WebSocket routes.
956
+ * Supports declarative route definition with automatic method detection.
957
+ *
958
+ * @example
959
+ * router.addRoutes({
960
+ * 'api': {
961
+ * 'users': {
962
+ * GET: (req, res) => ({ users: [] }),
963
+ * POST: (req, res) => ({ created: true })
964
+ * }
965
+ * }
966
+ * });
967
+ */
968
+ addRoutes(routeConfig) {
969
+ processRoutes(routeConfig, this);
970
+ }
971
+ /**
972
+ * Add global middleware to the router
973
+ *
974
+ * @param {Function|Object} middleware - Middleware function or conditional middleware object
975
+ * @description Adds middleware that runs before all route handlers. Supports both
976
+ * simple functions and conditional middleware objects.
977
+ *
978
+ * @example
979
+ * // Simple middleware
980
+ * router.use((req, res) => {
981
+ * console.log(`${req.method} ${req.url}`);
982
+ * });
983
+ *
984
+ * // Conditional middleware
985
+ * router.use({
986
+ * condition: (req) => req.url.startsWith('/api'),
987
+ * middleware: authMiddleware,
988
+ * name: 'apiAuth'
989
+ * });
990
+ */
991
+ use(middleware) {
992
+ if (typeof middleware === "function") {
993
+ this.globalMiddleware.push(middleware);
994
+ } else if (middleware && typeof middleware === "object") {
995
+ this.globalMiddleware.push(this.createConditionalMiddleware(middleware));
996
+ }
997
+ }
998
+ /**
999
+ * Create conditional middleware wrapper
1000
+ * @param {Object} config - Conditional middleware configuration
1001
+ * @returns {Function} Wrapped middleware function
1002
+ * @private
1003
+ */
1004
+ createConditionalMiddleware(config) {
1005
+ const { condition, middleware } = config;
1006
+ return async (req, res) => {
1007
+ let shouldExecute = false;
1008
+ if (typeof condition === "function") {
1009
+ shouldExecute = await condition(req, res);
1010
+ } else if (typeof condition === "object") {
1011
+ shouldExecute = this.evaluateConditionObject(condition, req);
1012
+ } else {
1013
+ shouldExecute = !!condition;
1014
+ }
1015
+ if (shouldExecute) {
1016
+ return await middleware(req, res);
1017
+ }
1018
+ return null;
1019
+ };
1020
+ }
1021
+ /**
1022
+ * Evaluate condition object
1023
+ * @param {Object} condition - Condition object
1024
+ * @param {Object} req - Request object
1025
+ * @param {Object} res - Response object
1026
+ * @returns {boolean} Whether condition is met
1027
+ * @private
1028
+ */
1029
+ evaluateConditionObject(condition, req) {
1030
+ const { method, path, header, query, body, user } = condition;
1031
+ if (method && !this.matchCondition(req.method, method)) return false;
1032
+ if (path && !this.matchCondition(req.url, path)) return false;
1033
+ if (header) {
1034
+ for (const [key, value] of Object.entries(header)) {
1035
+ if (!this.matchCondition(req.headers[key.toLowerCase()], value)) return false;
1036
+ }
1037
+ }
1038
+ if (query && req.query) {
1039
+ for (const [key, value] of Object.entries(query)) {
1040
+ if (!this.matchCondition(req.query[key], value)) return false;
1041
+ }
1042
+ }
1043
+ if (body && req.body) {
1044
+ for (const [key, value] of Object.entries(body)) {
1045
+ if (!this.matchCondition(req.body[key], value)) return false;
1046
+ }
1047
+ }
1048
+ if (user && req.user) {
1049
+ for (const [key, value] of Object.entries(user)) {
1050
+ if (!this.matchCondition(req.user[key], value)) return false;
1051
+ }
1052
+ }
1053
+ return true;
1054
+ }
1055
+ /**
1056
+ * Match condition value
1057
+ * @param {*} actual - Actual value
1058
+ * @param {*} expected - Expected value or condition
1059
+ * @returns {boolean} Whether condition matches
1060
+ * @private
1061
+ */
1062
+ matchCondition(actual, expected) {
1063
+ if (expected instanceof RegExp) {
1064
+ return expected.test(String(actual || ""));
1065
+ }
1066
+ if (Array.isArray(expected)) {
1067
+ return expected.includes(actual);
1068
+ }
1069
+ if (typeof expected === "function") {
1070
+ return expected(actual);
1071
+ }
1072
+ return actual === expected;
1073
+ }
1074
+ /**
1075
+ * Create a route group with shared middleware and prefix
1076
+ * @param {string} prefix - Path prefix for the group
1077
+ * @param {Function|Array} middleware - Shared middleware
1078
+ * @param {Function} callback - Function to define routes in the group
1079
+ */
1080
+ group(prefix, middleware, callback) {
1081
+ const group = {
1082
+ prefix: prefix.startsWith("/") ? prefix : `/${prefix}`,
1083
+ middleware: Array.isArray(middleware) ? middleware : [middleware]
1084
+ };
1085
+ this.routeGroups.push(group);
1086
+ callback(this);
1087
+ this.routeGroups.pop();
1088
+ return this;
1089
+ }
1090
+ /**
1091
+ * Get current route prefix from active groups
1092
+ * @private
1093
+ */
1094
+ getCurrentPrefix() {
1095
+ return this.routeGroups.map((g) => g.prefix).join("");
1096
+ }
1097
+ /**
1098
+ * Get current group middleware
1099
+ * @private
1100
+ */
1101
+ getCurrentGroupMiddleware() {
1102
+ return this.routeGroups.flatMap((g) => g.middleware);
1103
+ }
1104
+ /**
1105
+ * Compile route pattern into optimized regex
1106
+ * @param {string} pattern - Route pattern to compile
1107
+ * @returns {Object} Compiled route object with regex and parameter names
1108
+ * @private
1109
+ */
1110
+ compileRoute(pattern) {
1111
+ if (this.routeCompilationCache.has(pattern)) {
1112
+ if (this.enableMetrics) this.metrics.compilationHits++;
1113
+ return this.routeCompilationCache.get(pattern);
1114
+ }
1115
+ const paramNames = [];
1116
+ let regexPattern = pattern;
1117
+ if (pattern.includes("**")) {
1118
+ regexPattern = regexPattern.replace(/\/\*\*/g, "/(.*)");
1119
+ paramNames.push("splat");
1120
+ } else if (pattern.includes("*")) {
1121
+ regexPattern = regexPattern.replace(/\/\*/g, "/([^/]+)");
1122
+ paramNames.push("splat");
1123
+ }
1124
+ regexPattern = regexPattern.replace(/:([^(/]+)(\([^)]+\))?(\?)?/g, (match, paramName, constraint, optional) => {
1125
+ paramNames.push(paramName);
1126
+ if (constraint) {
1127
+ const constraintPattern = constraint.slice(1, -1);
1128
+ return optional ? `(?:/(?:${constraintPattern}))?` : `/(${constraintPattern})`;
1129
+ } else {
1130
+ return optional ? "(?:/([^/]+))?" : "/([^/]+)";
1131
+ }
1132
+ });
1133
+ regexPattern = regexPattern.replace(/[.+?^${}|[\]\\]/g, "\\$&").replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\\?/g, "?");
1134
+ regexPattern = `^${regexPattern}$`;
1135
+ const compiled = {
1136
+ regex: new RegExp(regexPattern),
1137
+ paramNames,
1138
+ pattern
1139
+ };
1140
+ if (this.routeCompilationCache.size < 1e3) {
1141
+ this.routeCompilationCache.set(pattern, compiled);
1142
+ }
1143
+ return compiled;
1144
+ }
1145
+ /**
1146
+ * Match path using compiled route
1147
+ * @param {Object} compiledRoute - Compiled route object
1148
+ * @param {string} path - Path to match
1149
+ * @returns {Object|null} Parameters object or null if no match
1150
+ * @private
1151
+ */
1152
+ matchCompiledRoute(compiledRoute, path) {
1153
+ const match = compiledRoute.regex.exec(path);
1154
+ if (!match) return null;
1155
+ const params = {};
1156
+ for (let i = 0; i < compiledRoute.paramNames.length; i++) {
1157
+ const paramName = compiledRoute.paramNames[i];
1158
+ const value = match[i + 1];
1159
+ if (value !== void 0) {
1160
+ params[paramName] = value;
1161
+ }
1162
+ }
1163
+ return params;
1164
+ }
1165
+ /**
1166
+ * Get performance metrics
1167
+ * @returns {Object} Performance metrics object
1168
+ */
1169
+ getMetrics() {
1170
+ if (!this.enableMetrics) {
1171
+ throw new Error("Metrics collection is disabled. Enable with { enableMetrics: true }");
1172
+ }
1173
+ const avgResponseTime = this.metrics.responseTime.length > 0 ? this.metrics.responseTime.reduce((a, b) => a + b, 0) / this.metrics.responseTime.length : 0;
1174
+ return {
1175
+ ...this.metrics,
1176
+ averageResponseTime: Math.round(avgResponseTime * 100) / 100,
1177
+ cacheHitRate: this.metrics.requests > 0 ? `${(this.metrics.cacheHits / this.metrics.requests * 100).toFixed(2)}%` : "0%",
1178
+ compilationHitRate: this.metrics.requests > 0 ? `${(this.metrics.compilationHits / this.metrics.requests * 100).toFixed(2)}%` : "0%"
1179
+ };
1180
+ }
1181
+ /**
1182
+ * Get compilation statistics
1183
+ * @returns {Object} Compilation statistics
1184
+ */
1185
+ getCompilationStats() {
1186
+ const totalRoutes = this.routes.length;
1187
+ const compiledRoutes = this.routes.filter((r) => r.compiled).length;
1188
+ const compilationCacheSize = this.routeCompilationCache.size;
1189
+ return {
1190
+ totalRoutes,
1191
+ compiledRoutes,
1192
+ compilationEnabled: this.enableCompilation,
1193
+ compilationCacheSize,
1194
+ compilationCacheHits: this.enableMetrics ? this.metrics.compilationHits : "N/A (metrics disabled)"
1195
+ };
1196
+ }
1197
+ /**
1198
+ * Clear route cache (useful for development)
1199
+ */
1200
+ clearCache() {
1201
+ this.routeCache.clear();
1202
+ }
1203
+ /**
1204
+ * Clear compilation cache
1205
+ */
1206
+ clearCompilationCache() {
1207
+ this.routeCompilationCache.clear();
1208
+ }
1209
+ /**
1210
+ * Get all registered routes with detailed information
1211
+ * @returns {Array} Array of route information objects
1212
+ */
1213
+ getRoutes() {
1214
+ return this.routes.map((route) => ({
1215
+ method: route.method,
1216
+ path: route.path,
1217
+ name: route.name || null,
1218
+ hasMiddleware: route.middleware && route.middleware.length > 0,
1219
+ middlewareCount: route.middleware ? route.middleware.length : 0,
1220
+ compiled: !!route.compiled,
1221
+ compiledPattern: route.compiled ? route.compiled.regex.source : null,
1222
+ paramNames: route.compiled ? route.compiled.paramNames : null
1223
+ }));
1224
+ }
1225
+ /**
1226
+ * Find routes matching a pattern or method
1227
+ * @param {Object} criteria - Search criteria
1228
+ * @returns {Array} Matching routes
1229
+ */
1230
+ findRoutes(criteria = {}) {
1231
+ const { method, path, name, hasMiddleware } = criteria;
1232
+ return this.routes.filter((route) => {
1233
+ if (method && route.method !== method.toUpperCase()) return false;
1234
+ if (path && !route.path.includes(path)) return false;
1235
+ if (name && route.name !== name) return false;
1236
+ if (hasMiddleware !== void 0 && !!route.middleware?.length !== hasMiddleware) return false;
1237
+ return true;
1238
+ }).map((route) => ({
1239
+ method: route.method,
1240
+ path: route.path,
1241
+ name: route.name || null,
1242
+ middlewareCount: route.middleware ? route.middleware.length : 0
1243
+ }));
1244
+ }
1245
+ /**
1246
+ * Test route matching without executing handlers
1247
+ * @param {string} method - HTTP method
1248
+ * @param {string} path - Path to test
1249
+ * @returns {Object} Match result with route info and extracted parameters
1250
+ */
1251
+ testRoute(method, path) {
1252
+ const upperMethod = method.toUpperCase();
1253
+ for (const route of this.routes) {
1254
+ if (route.method === upperMethod) {
1255
+ let params = null;
1256
+ if (this.enableCompilation && route.compiled) {
1257
+ params = this.matchCompiledRoute(route.compiled, path);
1258
+ } else {
1259
+ params = extractParams(route.path, path);
1260
+ }
1261
+ if (params !== null) {
1262
+ return {
1263
+ matched: true,
1264
+ route: {
1265
+ method: route.method,
1266
+ path: route.path,
1267
+ name: route.name || null,
1268
+ middlewareCount: route.middleware ? route.middleware.length : 0
1269
+ },
1270
+ params,
1271
+ compiledUsed: this.enableCompilation && !!route.compiled
1272
+ };
1273
+ }
1274
+ }
1275
+ }
1276
+ return { matched: false, route: null, params: null };
1277
+ }
1278
+ /**
1279
+ * Get router debug information
1280
+ * @returns {Object} Comprehensive debug information
1281
+ */
1282
+ getDebugInfo() {
1283
+ const routesByMethod = {};
1284
+ const namedRoutes = {};
1285
+ this.routes.forEach((route) => {
1286
+ if (!routesByMethod[route.method]) {
1287
+ routesByMethod[route.method] = [];
1288
+ }
1289
+ routesByMethod[route.method].push({
1290
+ path: route.path,
1291
+ name: route.name,
1292
+ middlewareCount: route.middleware ? route.middleware.length : 0,
1293
+ compiled: !!route.compiled
1294
+ });
1295
+ });
1296
+ this.namedRoutes.forEach((routeInfo, name) => {
1297
+ namedRoutes[name] = routeInfo;
1298
+ });
1299
+ return {
1300
+ totalRoutes: this.routes.length,
1301
+ routesByMethod,
1302
+ namedRoutes,
1303
+ globalMiddleware: this.globalMiddleware.length,
1304
+ activeGroups: this.routeGroups.length,
1305
+ cacheSize: this.routeCache.size,
1306
+ maxCacheSize: this.maxCacheSize,
1307
+ compilationEnabled: this.enableCompilation,
1308
+ compilationCacheSize: this.routeCompilationCache.size,
1309
+ metricsEnabled: this.enableMetrics
1310
+ };
1311
+ }
1312
+ async handle(req, res, options = {}) {
1313
+ const startTime = Date.now();
1314
+ if (this.enableMetrics) {
1315
+ this.metrics.requests++;
1316
+ }
1317
+ const { corsOrigin, rateLimit = { windowMs: 6e4, maxRequests: 100 } } = options;
1318
+ addSecurityHeaders(res, corsOrigin);
1319
+ if (req.method === "OPTIONS") {
1320
+ res.writeHead(204);
1321
+ res.end();
1322
+ return;
1323
+ }
1324
+ const clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress || "unknown";
1325
+ if (!checkRateLimit(clientIP, rateLimit.windowMs, rateLimit.maxRequests)) {
1326
+ res.writeHead(429, { "Content-Type": "application/json" });
1327
+ res.end(JSON.stringify({ _error: "Too Many Requests" }));
1328
+ return;
1329
+ }
1330
+ const parsedUrl = parseUrl(req.url, true);
1331
+ const pathname = parsedUrl.pathname;
1332
+ req.query = parsedUrl.query || {};
1333
+ try {
1334
+ req.body = await parseBody(req, options.maxBodySize);
1335
+ } catch (_error) {
1336
+ if (this.enableMetrics) this.metrics.errors++;
1337
+ const statusCode = _error.message.includes("too large") ? 413 : 400;
1338
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
1339
+ res.end(JSON.stringify({ _error: _error.message }));
1340
+ return;
1341
+ }
1342
+ const cacheKey = `${req.method}:${pathname}`;
1343
+ let matchedRoute = this.routeCache.get(cacheKey);
1344
+ if (matchedRoute && this.enableMetrics) {
1345
+ this.metrics.cacheHits++;
1346
+ }
1347
+ if (!matchedRoute) {
1348
+ const requestVersion = this.enableVersioning ? this.getRequestVersion(req) : null;
1349
+ if (this.enableMetrics && requestVersion) {
1350
+ this.metrics.versionRequests.set(requestVersion, (this.metrics.versionRequests.get(requestVersion) || 0) + 1);
1351
+ }
1352
+ const routesToSearch = this.enableVersioning && this.versionedRoutes.has(requestVersion) ? this.versionedRoutes.get(requestVersion) : this.routes;
1353
+ for (const route of routesToSearch) {
1354
+ if (route.method === req.method) {
1355
+ if (this.enableVersioning && route.version !== requestVersion) {
1356
+ continue;
1357
+ }
1358
+ let params = null;
1359
+ if (this.enableCompilation && route.compiled) {
1360
+ params = this.matchCompiledRoute(route.compiled, pathname);
1361
+ } else {
1362
+ params = extractParams(route.path, pathname);
1363
+ }
1364
+ if (params !== null) {
1365
+ matchedRoute = { route, params };
1366
+ if (this.routeCache.size < this.maxCacheSize) {
1367
+ this.routeCache.set(cacheKey, matchedRoute);
1368
+ }
1369
+ break;
1370
+ }
1371
+ }
1372
+ }
1373
+ }
1374
+ if (matchedRoute) {
1375
+ req.params = matchedRoute.params;
1376
+ if (this.enableMetrics) {
1377
+ const routeKey = `${req.method}:${matchedRoute.route.path}`;
1378
+ this.metrics.routeMatches.set(routeKey, (this.metrics.routeMatches.get(routeKey) || 0) + 1);
1379
+ }
1380
+ console.log(`${(/* @__PURE__ */ new Date()).toISOString()} ${req.method} ${pathname}`);
1381
+ try {
1382
+ if (matchedRoute.route.middleware && matchedRoute.route.middleware.length > 0) {
1383
+ for (const middleware of matchedRoute.route.middleware) {
1384
+ const result2 = await middleware(req, res);
1385
+ if (result2) break;
1386
+ }
1387
+ }
1388
+ const { route } = matchedRoute;
1389
+ const result = await route.handler(req, res);
1390
+ if (result && typeof result === "object" && !res.headersSent) {
1391
+ res.writeHead(200, { "Content-Type": "application/json" });
1392
+ res.end(JSON.stringify(result));
1393
+ }
1394
+ if (this.enableMetrics) {
1395
+ const responseTime = Date.now() - startTime;
1396
+ this.metrics.responseTime.push(responseTime);
1397
+ if (this.metrics.responseTime.length > 1e3) {
1398
+ this.metrics.responseTime = this.metrics.responseTime.slice(-1e3);
1399
+ }
1400
+ }
1401
+ return;
1402
+ } catch (_error) {
1403
+ if (this.enableMetrics) this.metrics.errors++;
1404
+ if (!res.headersSent) {
1405
+ res.writeHead(500, { "Content-Type": "application/json" });
1406
+ res.end(JSON.stringify({ _error: _error.message }));
1407
+ }
1408
+ return;
1409
+ }
1410
+ }
1411
+ if (this.enableMetrics) this.metrics.errors++;
1412
+ if (!res.headersSent) {
1413
+ res.writeHead(404, { "Content-Type": "application/json" });
1414
+ res.end(JSON.stringify({ _error: "Not Found" }));
1415
+ }
1416
+ }
1417
+ createServer(options = {}) {
1418
+ const mergedOptions = { ...this.defaultOptions, ...options };
1419
+ return createServer((req, res) => this.handle(req, res, mergedOptions));
1420
+ }
1421
+ // HTTP convenience methods
1422
+ get(path, handler, options = {}) {
1423
+ return this.addRoute("GET", path, handler, options);
1424
+ }
1425
+ post(path, handler, options = {}) {
1426
+ return this.addRoute("POST", path, handler, options);
1427
+ }
1428
+ put(path, handler, options = {}) {
1429
+ return this.addRoute("PUT", path, handler, options);
1430
+ }
1431
+ patch(path, handler, options = {}) {
1432
+ return this.addRoute("PATCH", path, handler, options);
1433
+ }
1434
+ delete(path, handler, options = {}) {
1435
+ return this.addRoute("DELETE", path, handler, options);
1436
+ }
1437
+ options(path, handler, options = {}) {
1438
+ return this.addRoute("OPTIONS", path, handler, options);
1439
+ }
1440
+ head(path, handler, options = {}) {
1441
+ return this.addRoute("HEAD", path, handler, options);
1442
+ }
1443
+ };
1444
+ function createRouter(routeConfig, options = {}) {
1445
+ const router = new SimpleRouter(options);
1446
+ router.defaultOptions = options;
1447
+ if (routeConfig) {
1448
+ router.addRoutes(routeConfig);
1449
+ }
1450
+ return router;
1451
+ }
1452
+
1453
+ // src/serialization.js
1454
+ function serializeDate(date) {
1455
+ if (!(date instanceof Date)) {
1456
+ throw new Error("Expected Date object");
1457
+ }
1458
+ return date.toISOString();
1459
+ }
1460
+ function deserializeDate(dateString) {
1461
+ const date = new Date(dateString);
1462
+ if (isNaN(date.getTime())) {
1463
+ throw new Error("Invalid date string");
1464
+ }
1465
+ return date;
1466
+ }
1467
+ function serializeMap(map) {
1468
+ if (!(map instanceof Map)) {
1469
+ throw new Error("Expected Map object");
1470
+ }
1471
+ const obj = {};
1472
+ for (const [key, value] of map) {
1473
+ obj[key] = value;
1474
+ }
1475
+ return obj;
1476
+ }
1477
+ function deserializeMap(obj) {
1478
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
1479
+ throw new Error("Expected plain object");
1480
+ }
1481
+ return new Map(Object.entries(obj));
1482
+ }
1483
+ function serializeSet(set) {
1484
+ if (!(set instanceof Set)) {
1485
+ throw new Error("Expected Set object");
1486
+ }
1487
+ return Array.from(set);
1488
+ }
1489
+ function deserializeSet(arr) {
1490
+ if (!Array.isArray(arr)) {
1491
+ throw new Error("Expected array");
1492
+ }
1493
+ return new Set(arr);
1494
+ }
1495
+ function withSerialization(options = {}) {
1496
+ const {
1497
+ enableDate = true,
1498
+ enableMap = true,
1499
+ enableSet = true,
1500
+ custom = {}
1501
+ } = options;
1502
+ return (req, res, next) => {
1503
+ res.serialize = {};
1504
+ req.deserialize = {};
1505
+ if (enableDate) {
1506
+ res.serialize.date = custom.serializeDate || serializeDate;
1507
+ req.deserialize.date = custom.deserializeDate || deserializeDate;
1508
+ }
1509
+ if (enableMap) {
1510
+ res.serialize.map = custom.serializeMap || serializeMap;
1511
+ req.deserialize.map = custom.deserializeMap || deserializeMap;
1512
+ }
1513
+ if (enableSet) {
1514
+ res.serialize.set = custom.serializeSet || serializeSet;
1515
+ req.deserialize.set = custom.deserializeSet || deserializeSet;
1516
+ }
1517
+ next();
1518
+ };
1519
+ }
1520
+ function serializeForJSON(data) {
1521
+ if (data instanceof Date) {
1522
+ return serializeDate(data);
1523
+ }
1524
+ if (data instanceof Map) {
1525
+ return serializeMap(data);
1526
+ }
1527
+ if (data instanceof Set) {
1528
+ return serializeSet(data);
1529
+ }
1530
+ if (Array.isArray(data)) {
1531
+ return data.map((item) => serializeForJSON(item));
1532
+ }
1533
+ if (typeof data === "object" && data !== null) {
1534
+ const serialized = {};
1535
+ for (const [key, value] of Object.entries(data)) {
1536
+ serialized[key] = serializeForJSON(value);
1537
+ }
1538
+ return serialized;
1539
+ }
1540
+ return data;
1541
+ }
1542
+
1543
+ // src/security.js
1544
+ import { createHmac, randomBytes as randomBytes2, pbkdf2Sync } from "crypto";
1545
+ import { Buffer as Buffer2 } from "buffer";
1546
+ function base64UrlDecode(str) {
1547
+ str += "=".repeat((4 - str.length % 4) % 4);
1548
+ return Buffer2.from(str.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString();
1549
+ }
1550
+ function createSignature(data, secret) {
1551
+ return createHmac("sha256", secret).update(data).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1552
+ }
1553
+ function verifyToken(token, secret = "your-secret-key") {
1554
+ try {
1555
+ let jwtToken = token;
1556
+ if (token && token.startsWith("Bearer ")) {
1557
+ jwtToken = token.slice(7);
1558
+ }
1559
+ if (!jwtToken) {
1560
+ return null;
1561
+ }
1562
+ const parts = jwtToken.split(".");
1563
+ if (parts.length !== 3) {
1564
+ return null;
1565
+ }
1566
+ const [encodedHeader, encodedPayload, signature] = parts;
1567
+ const data = `${encodedHeader}.${encodedPayload}`;
1568
+ const expectedSignature = createSignature(data, secret);
1569
+ if (signature !== expectedSignature) {
1570
+ return null;
1571
+ }
1572
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
1573
+ const now = Math.floor(Date.now() / 1e3);
1574
+ if (payload.exp && payload.exp < now) {
1575
+ return null;
1576
+ }
1577
+ return payload;
1578
+ } catch {
1579
+ return null;
1580
+ }
1581
+ }
1582
+ function withAuth(options = {}) {
1583
+ const { secret, required = true } = options;
1584
+ return (req, res) => {
1585
+ const authHeader = req.headers.authorization;
1586
+ const user = verifyToken(authHeader, secret);
1587
+ if (required && !user) {
1588
+ res.writeHead(401, { "Content-Type": "application/json" });
1589
+ res.end(JSON.stringify({ _error: "Unauthorized" }));
1590
+ return;
1591
+ }
1592
+ req.user = user;
1593
+ return null;
1594
+ };
1595
+ }
1596
+ function withRole(roles) {
1597
+ const requiredRoles = Array.isArray(roles) ? roles : [roles];
1598
+ return (req, res) => {
1599
+ if (!req.user) {
1600
+ res.writeHead(401, { "Content-Type": "application/json" });
1601
+ res.end(JSON.stringify({ _error: "Unauthorized" }));
1602
+ return;
1603
+ }
1604
+ if (!requiredRoles.includes(req.user.role)) {
1605
+ res.writeHead(403, { "Content-Type": "application/json" });
1606
+ res.end(JSON.stringify({ _error: "Forbidden" }));
1607
+ return;
1608
+ }
1609
+ return null;
1610
+ };
1611
+ }
1612
+ function hashPassword(password) {
1613
+ const salt = randomBytes2(16).toString("hex");
1614
+ const hash = pbkdf2Sync(password, salt, 1e4, 64, "sha512").toString("hex");
1615
+ return `${salt}:${hash}`;
1616
+ }
1617
+ function verifyPassword(password, hashedPassword) {
1618
+ try {
1619
+ const [salt, hash] = hashedPassword.split(":");
1620
+ const verifyHash = pbkdf2Sync(password, salt, 1e4, 64, "sha512").toString("hex");
1621
+ return hash === verifyHash;
1622
+ } catch {
1623
+ return false;
1624
+ }
1625
+ }
1626
+ function generateToken(length = 32) {
1627
+ return randomBytes2(length).toString("hex");
1628
+ }
1629
+ function withInputValidation(rules) {
1630
+ return (req, res) => {
1631
+ const errors = [];
1632
+ for (const [field, rule] of Object.entries(rules)) {
1633
+ const value = req.body[field];
1634
+ if (rule.required && (value === void 0 || value === null || value === "")) {
1635
+ errors.push(`${field} is required`);
1636
+ continue;
1637
+ }
1638
+ if (value !== void 0 && rule.type && typeof value !== rule.type) {
1639
+ errors.push(`${field} must be of type ${rule.type}`);
1640
+ }
1641
+ if (value && rule.minLength && value.length < rule.minLength) {
1642
+ errors.push(`${field} must be at least ${rule.minLength} characters`);
1643
+ }
1644
+ if (value && rule.maxLength && value.length > rule.maxLength) {
1645
+ errors.push(`${field} must be at most ${rule.maxLength} characters`);
1646
+ }
1647
+ if (value && rule.pattern && !rule.pattern.test(value)) {
1648
+ errors.push(`${field} format is invalid`);
1649
+ }
1650
+ }
1651
+ if (errors.length > 0) {
1652
+ res.writeHead(400, { "Content-Type": "application/json" });
1653
+ res.end(JSON.stringify({ _error: "Validation failed", details: errors }));
1654
+ return;
1655
+ }
1656
+ return null;
1657
+ };
1658
+ }
1659
+
1660
+ // src/index.js
1661
+ var index_default = {
1662
+ createRouter,
1663
+ ApiError,
1664
+ ValidationError,
1665
+ AuthenticationError,
1666
+ AuthorizationError,
1667
+ NotFoundError,
1668
+ ConflictError,
1669
+ withErrorHandling,
1670
+ createErrorHandler,
1671
+ validateAgainstSchema,
1672
+ validateField,
1673
+ withValidation,
1674
+ withQueryValidation,
1675
+ withParamsValidation,
1676
+ serializeDate,
1677
+ deserializeDate,
1678
+ serializeMap,
1679
+ deserializeMap,
1680
+ serializeSet,
1681
+ deserializeSet,
1682
+ withSerialization,
1683
+ serializeForJSON,
1684
+ withAuth,
1685
+ withRole,
1686
+ hashPassword,
1687
+ verifyPassword,
1688
+ generateToken,
1689
+ withInputValidation
1690
+ };
1691
+ export {
1692
+ ApiError,
1693
+ AuthenticationError,
1694
+ AuthorizationError,
1695
+ ConflictError,
1696
+ NotFoundError,
1697
+ ValidationError,
1698
+ createErrorHandler,
1699
+ createRouter,
1700
+ index_default as default,
1701
+ deserializeDate,
1702
+ deserializeMap,
1703
+ deserializeSet,
1704
+ generateToken,
1705
+ hashPassword,
1706
+ serializeDate,
1707
+ serializeForJSON,
1708
+ serializeMap,
1709
+ serializeSet,
1710
+ validateAgainstSchema,
1711
+ validateField,
1712
+ verifyPassword,
1713
+ withAuth,
1714
+ withErrorHandling,
1715
+ withInputValidation,
1716
+ withParamsValidation,
1717
+ withQueryValidation,
1718
+ withRole,
1719
+ withSerialization,
1720
+ withValidation
1721
+ };
1722
+ //# sourceMappingURL=index.js.map