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