@morojs/moro 1.5.7 → 1.5.9

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 (39) hide show
  1. package/dist/core/database/adapters/mongodb.d.ts +10 -0
  2. package/dist/core/database/adapters/mongodb.js +23 -2
  3. package/dist/core/database/adapters/mongodb.js.map +1 -1
  4. package/dist/core/database/adapters/mysql.d.ts +11 -0
  5. package/dist/core/database/adapters/mysql.js +1 -0
  6. package/dist/core/database/adapters/mysql.js.map +1 -1
  7. package/dist/core/database/adapters/postgresql.d.ts +9 -1
  8. package/dist/core/database/adapters/postgresql.js +1 -1
  9. package/dist/core/database/adapters/postgresql.js.map +1 -1
  10. package/dist/core/database/adapters/redis.d.ts +9 -0
  11. package/dist/core/database/adapters/redis.js +14 -4
  12. package/dist/core/database/adapters/redis.js.map +1 -1
  13. package/dist/core/framework.js +61 -0
  14. package/dist/core/framework.js.map +1 -1
  15. package/dist/core/http/http-server.js +83 -14
  16. package/dist/core/http/http-server.js.map +1 -1
  17. package/dist/core/middleware/built-in/cache.js +3 -1
  18. package/dist/core/middleware/built-in/cache.js.map +1 -1
  19. package/dist/core/middleware/built-in/sse.js +9 -7
  20. package/dist/core/middleware/built-in/sse.js.map +1 -1
  21. package/dist/core/modules/modules.js +10 -0
  22. package/dist/core/modules/modules.js.map +1 -1
  23. package/dist/core/runtime/node-adapter.js +12 -6
  24. package/dist/core/runtime/node-adapter.js.map +1 -1
  25. package/dist/types/http.d.ts +16 -1
  26. package/dist/types/module.d.ts +11 -0
  27. package/package.json +1 -1
  28. package/src/core/database/adapters/mongodb.ts +30 -2
  29. package/src/core/database/adapters/mysql.ts +14 -0
  30. package/src/core/database/adapters/postgresql.ts +12 -2
  31. package/src/core/database/adapters/redis.ts +27 -4
  32. package/src/core/framework.ts +70 -0
  33. package/src/core/http/http-server.ts +95 -14
  34. package/src/core/middleware/built-in/cache.ts +3 -1
  35. package/src/core/middleware/built-in/sse.ts +9 -7
  36. package/src/core/modules/modules.ts +15 -0
  37. package/src/core/runtime/node-adapter.ts +12 -6
  38. package/src/types/http.ts +23 -1
  39. package/src/types/module.ts +12 -0
@@ -19,8 +19,17 @@ export interface CookieOptions {
19
19
  sameSite?: 'strict' | 'lax' | 'none';
20
20
  domain?: string;
21
21
  path?: string;
22
+ critical?: boolean;
23
+ throwOnLateSet?: boolean;
22
24
  }
23
- export interface HttpResponse extends ServerResponse {
25
+ export interface ResponseState {
26
+ headersSent: boolean;
27
+ statusCode: number;
28
+ headers: Record<string, any>;
29
+ finished: boolean;
30
+ writable: boolean;
31
+ }
32
+ export interface MoroResponseMethods {
24
33
  json(data: any): void;
25
34
  status(code: number): HttpResponse;
26
35
  send(data: string | Buffer): void;
@@ -29,7 +38,13 @@ export interface HttpResponse extends ServerResponse {
29
38
  redirect(url: string, status?: number): void;
30
39
  sendFile(filePath: string): Promise<void>;
31
40
  render?(template: string, data?: any): Promise<void>;
41
+ hasHeader(name: string): boolean;
42
+ setBulkHeaders(headers: Record<string, string | number>): HttpResponse;
43
+ appendHeader(name: string, value: string | string[]): HttpResponse;
44
+ canSetHeaders(): boolean;
45
+ getResponseState(): ResponseState;
32
46
  }
47
+ export type HttpResponse = ServerResponse & MoroResponseMethods;
33
48
  export type HttpHandler = (req: HttpRequest, res: HttpResponse) => Promise<void> | void;
34
49
  export type Middleware = (req: HttpRequest, res: HttpResponse, next: () => void) => Promise<void> | void;
35
50
  export type MiddlewareFunction = (req: HttpRequest, res: HttpResponse, next: () => void) => void | Promise<void>;
@@ -12,6 +12,11 @@ export interface ModuleRoute {
12
12
  window: number;
13
13
  };
14
14
  middleware?: string[];
15
+ auth?: {
16
+ roles?: string[];
17
+ permissions?: string[];
18
+ optional?: boolean;
19
+ };
15
20
  }
16
21
  export interface ModuleSocket {
17
22
  event: string;
@@ -65,6 +70,12 @@ export interface InternalRouteDefinition {
65
70
  validation?: any;
66
71
  cache?: CacheConfig;
67
72
  rateLimit?: RateLimitConfig;
73
+ auth?: {
74
+ roles?: string[];
75
+ permissions?: string[];
76
+ optional?: boolean;
77
+ };
78
+ [key: string]: any;
68
79
  }
69
80
  export interface WebSocketDefinition {
70
81
  event: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morojs/moro",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "description": "High-performance Node.js framework with intelligent routing, automatic middleware ordering, enterprise authentication (Auth.js), type-safe Zod validation, and functional architecture",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,6 +11,16 @@ interface MongoDBConfig {
11
11
  database?: string;
12
12
  authSource?: string;
13
13
  ssl?: boolean;
14
+ tls?: {
15
+ ca?: string;
16
+ cert?: string;
17
+ key?: string;
18
+ passphrase?: string;
19
+ insecure?: boolean;
20
+ allowInvalidCertificates?: boolean;
21
+ allowInvalidHostnames?: boolean;
22
+ checkServerIdentity?: boolean;
23
+ };
14
24
  replicaSet?: string;
15
25
  maxPoolSize?: number;
16
26
  minPoolSize?: number;
@@ -32,11 +42,29 @@ export class MongoDBAdapter implements DatabaseAdapter {
32
42
 
33
43
  const url = config.url || this.buildConnectionString(config);
34
44
 
35
- this.client = new MongoClient(url, {
45
+ const clientOptions: any = {
36
46
  maxPoolSize: config.maxPoolSize || 10,
37
47
  minPoolSize: config.minPoolSize || 0,
38
48
  ssl: config.ssl || false,
39
- });
49
+ };
50
+
51
+ // Add TLS options if provided
52
+ if (config.tls) {
53
+ clientOptions.tls = true;
54
+ if (config.tls.ca) clientOptions.tlsCAFile = config.tls.ca;
55
+ if (config.tls.cert) clientOptions.tlsCertificateFile = config.tls.cert;
56
+ if (config.tls.key) clientOptions.tlsCertificateKeyFile = config.tls.key;
57
+ if (config.tls.passphrase)
58
+ clientOptions.tlsCertificateKeyFilePassword = config.tls.passphrase;
59
+ if (config.tls.insecure) clientOptions.tlsInsecure = config.tls.insecure;
60
+ if (config.tls.allowInvalidCertificates)
61
+ clientOptions.tlsAllowInvalidCertificates = config.tls.allowInvalidCertificates;
62
+ if (config.tls.allowInvalidHostnames)
63
+ clientOptions.tlsAllowInvalidHostnames = config.tls.allowInvalidHostnames;
64
+ if (config.tls.checkServerIdentity === false) clientOptions.checkServerIdentity = false;
65
+ }
66
+
67
+ this.client = new MongoClient(url, clientOptions);
40
68
 
41
69
  this.db = this.client.db(config.database || 'moro_app');
42
70
 
@@ -9,6 +9,19 @@ interface MySQLConfig {
9
9
  password?: string;
10
10
  database?: string;
11
11
  connectionLimit?: number;
12
+ ssl?:
13
+ | {
14
+ rejectUnauthorized?: boolean;
15
+ ca?: string;
16
+ cert?: string;
17
+ key?: string;
18
+ passphrase?: string;
19
+ servername?: string;
20
+ checkServerIdentity?: boolean;
21
+ ciphers?: string;
22
+ secureProtocol?: string;
23
+ }
24
+ | boolean;
12
25
  }
13
26
 
14
27
  export class MySQLAdapter implements DatabaseAdapter {
@@ -27,6 +40,7 @@ export class MySQLAdapter implements DatabaseAdapter {
27
40
  waitForConnections: true,
28
41
  connectionLimit: config.connectionLimit || 10,
29
42
  queueLimit: 0,
43
+ ssl: typeof config.ssl === 'object' ? { ...config.ssl } : config.ssl || false,
30
44
  });
31
45
  } catch (error) {
32
46
  throw new Error(
@@ -9,7 +9,17 @@ interface PostgreSQLConfig {
9
9
  password?: string;
10
10
  database?: string;
11
11
  connectionLimit?: number;
12
- ssl?: boolean;
12
+ ssl?:
13
+ | {
14
+ rejectUnauthorized?: boolean;
15
+ ca?: string;
16
+ cert?: string;
17
+ key?: string;
18
+ passphrase?: string;
19
+ servername?: string;
20
+ checkServerIdentity?: boolean;
21
+ }
22
+ | boolean;
13
23
  }
14
24
 
15
25
  export class PostgreSQLAdapter implements DatabaseAdapter {
@@ -26,7 +36,7 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
26
36
  password: config.password || '',
27
37
  database: config.database || 'moro_app',
28
38
  max: config.connectionLimit || 10,
29
- ssl: config.ssl || false,
39
+ ssl: typeof config.ssl === 'object' ? { ...config.ssl } : config.ssl || false,
30
40
  });
31
41
 
32
42
  this.pool.on('error', (err: Error) => {
@@ -11,6 +11,15 @@ interface RedisConfig {
11
11
  maxRetriesPerRequest?: number;
12
12
  retryDelayOnFailover?: number;
13
13
  lazyConnect?: boolean;
14
+ tls?: {
15
+ rejectUnauthorized?: boolean;
16
+ ca?: string;
17
+ cert?: string;
18
+ key?: string;
19
+ passphrase?: string;
20
+ servername?: string;
21
+ checkServerIdentity?: boolean;
22
+ };
14
23
  cluster?: {
15
24
  enableReadyCheck?: boolean;
16
25
  redisOptions?: any;
@@ -30,13 +39,20 @@ export class RedisAdapter implements DatabaseAdapter {
30
39
 
31
40
  if (config.cluster) {
32
41
  // Redis Cluster
33
- this.client = new Redis.Cluster(config.cluster.nodes, {
42
+ const clusterOptions: any = {
34
43
  enableReadyCheck: config.cluster.enableReadyCheck || false,
35
44
  redisOptions: config.cluster.redisOptions || {},
36
- });
45
+ };
46
+
47
+ // Add TLS options to cluster configuration
48
+ if (config.tls) {
49
+ clusterOptions.redisOptions.tls = { ...config.tls };
50
+ }
51
+
52
+ this.client = new Redis.Cluster(config.cluster.nodes, clusterOptions);
37
53
  } else {
38
54
  // Single Redis instance
39
- this.client = new Redis({
55
+ const redisOptions: any = {
40
56
  host: config.host || 'localhost',
41
57
  port: config.port || 6379,
42
58
  password: config.password,
@@ -44,7 +60,14 @@ export class RedisAdapter implements DatabaseAdapter {
44
60
  maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
45
61
  retryDelayOnFailover: config.retryDelayOnFailover || 100,
46
62
  lazyConnect: config.lazyConnect || true,
47
- });
63
+ };
64
+
65
+ // Add TLS options if provided
66
+ if (config.tls) {
67
+ redisOptions.tls = { ...config.tls };
68
+ }
69
+
70
+ this.client = new Redis(redisOptions);
48
71
  }
49
72
 
50
73
  this.client.on('error', (err: Error) => {
@@ -444,6 +444,76 @@ export class Moro extends EventEmitter {
444
444
  throw new Error(`Handler ${route.handler} is not a function`);
445
445
  }
446
446
 
447
+ // Check authentication if auth configuration is provided
448
+ if ((route as any).auth) {
449
+ const auth = (req as any).auth;
450
+ const authConfig = (route as any).auth;
451
+
452
+ if (!auth) {
453
+ res.status(401);
454
+ res.json({
455
+ success: false,
456
+ error: 'Authentication required',
457
+ message: 'You must be logged in to access this resource',
458
+ });
459
+ return;
460
+ }
461
+
462
+ // Check authentication requirement (default is required unless optional: true)
463
+ if (!authConfig.optional && !auth.isAuthenticated) {
464
+ res.status(401);
465
+ res.json({
466
+ success: false,
467
+ error: 'Authentication required',
468
+ message: 'You must be logged in to access this resource',
469
+ });
470
+ return;
471
+ }
472
+
473
+ // Skip further checks if not authenticated but optional
474
+ if (!auth.isAuthenticated && authConfig.optional) {
475
+ // Continue to handler
476
+ } else if (auth.isAuthenticated) {
477
+ const user = auth.user;
478
+
479
+ // Check roles if specified
480
+ if (authConfig.roles && authConfig.roles.length > 0) {
481
+ const userRoles = user?.roles || [];
482
+ const hasRole = authConfig.roles.some((role: string) => userRoles.includes(role));
483
+
484
+ if (!hasRole) {
485
+ res.status(403);
486
+ res.json({
487
+ success: false,
488
+ error: 'Insufficient permissions',
489
+ message: `Required roles: ${authConfig.roles.join(', ')}`,
490
+ userRoles,
491
+ });
492
+ return;
493
+ }
494
+ }
495
+
496
+ // Check permissions if specified
497
+ if (authConfig.permissions && authConfig.permissions.length > 0) {
498
+ const userPermissions = user?.permissions || [];
499
+ const hasPermission = authConfig.permissions.every((permission: string) =>
500
+ userPermissions.includes(permission)
501
+ );
502
+
503
+ if (!hasPermission) {
504
+ res.status(403);
505
+ res.json({
506
+ success: false,
507
+ error: 'Insufficient permissions',
508
+ message: `Required permissions: ${authConfig.permissions.join(', ')}`,
509
+ userPermissions,
510
+ });
511
+ return;
512
+ }
513
+ }
514
+ }
515
+ }
516
+
447
517
  // Validate request if validation schema is provided
448
518
  if (route.validation) {
449
519
  try {
@@ -631,6 +631,26 @@ export class MoroHttpServer {
631
631
  };
632
632
 
633
633
  httpRes.cookie = (name: string, value: string, options: any = {}) => {
634
+ if (httpRes.headersSent) {
635
+ const isCritical =
636
+ options.critical ||
637
+ name.includes('session') ||
638
+ name.includes('auth') ||
639
+ name.includes('csrf');
640
+ const message = `Cookie '${name}' could not be set - headers already sent`;
641
+
642
+ if (isCritical || options.throwOnLateSet) {
643
+ throw new Error(`${message}. This may cause authentication or security issues.`);
644
+ } else {
645
+ this.logger.warn(message, 'CookieWarning', {
646
+ cookieName: name,
647
+ critical: isCritical,
648
+ stackTrace: new Error().stack,
649
+ });
650
+ }
651
+ return httpRes;
652
+ }
653
+
634
654
  const cookieValue = encodeURIComponent(value);
635
655
  let cookieString = `${name}=${cookieValue}`;
636
656
 
@@ -694,6 +714,62 @@ export class MoroHttpServer {
694
714
  }
695
715
  };
696
716
 
717
+ // Header management utilities
718
+ httpRes.hasHeader = (name: string): boolean => {
719
+ return httpRes.getHeader(name) !== undefined;
720
+ };
721
+
722
+ // Note: removeHeader is inherited from ServerResponse, we don't override it
723
+
724
+ httpRes.setBulkHeaders = (headers: Record<string, string | number>) => {
725
+ if (httpRes.headersSent) {
726
+ this.logger.warn('Cannot set headers - headers already sent', 'HeaderWarning', {
727
+ attemptedHeaders: Object.keys(headers),
728
+ });
729
+ return httpRes;
730
+ }
731
+
732
+ Object.entries(headers).forEach(([key, value]) => {
733
+ httpRes.setHeader(key, value);
734
+ });
735
+ return httpRes;
736
+ };
737
+
738
+ httpRes.appendHeader = (name: string, value: string | string[]) => {
739
+ if (httpRes.headersSent) {
740
+ this.logger.warn(
741
+ `Cannot append to header '${name}' - headers already sent`,
742
+ 'HeaderWarning'
743
+ );
744
+ return httpRes;
745
+ }
746
+
747
+ const existing = httpRes.getHeader(name);
748
+ if (existing) {
749
+ const values = Array.isArray(existing) ? existing : [existing.toString()];
750
+ const newValues = Array.isArray(value) ? value : [value];
751
+ httpRes.setHeader(name, [...values, ...newValues]);
752
+ } else {
753
+ httpRes.setHeader(name, value);
754
+ }
755
+ return httpRes;
756
+ };
757
+
758
+ // Response state utilities
759
+ httpRes.canSetHeaders = (): boolean => {
760
+ return !httpRes.headersSent;
761
+ };
762
+
763
+ httpRes.getResponseState = () => {
764
+ return {
765
+ headersSent: httpRes.headersSent,
766
+ statusCode: httpRes.statusCode,
767
+ headers: httpRes.getHeaders ? httpRes.getHeaders() : {},
768
+ finished: httpRes.finished || false,
769
+ writable: httpRes.writable,
770
+ };
771
+ };
772
+
697
773
  return httpRes;
698
774
  }
699
775
 
@@ -1075,23 +1151,25 @@ export const middleware = {
1075
1151
  }
1076
1152
 
1077
1153
  if (acceptEncoding.includes('gzip')) {
1078
- res.setHeader('Content-Encoding', 'gzip');
1079
1154
  zlib.gzip(buffer, { level }, (err: any, compressed: Buffer) => {
1080
1155
  if (err) {
1081
1156
  return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
1082
1157
  }
1083
- res.setHeader('Content-Length', compressed.length);
1084
- res.writeHead(res.statusCode || 200, res.getHeaders());
1158
+ if (!res.headersSent) {
1159
+ res.setHeader('Content-Encoding', 'gzip');
1160
+ res.setHeader('Content-Length', compressed.length);
1161
+ }
1085
1162
  res.end(compressed);
1086
1163
  });
1087
1164
  } else if (acceptEncoding.includes('deflate')) {
1088
- res.setHeader('Content-Encoding', 'deflate');
1089
1165
  zlib.deflate(buffer, { level }, (err: any, compressed: Buffer) => {
1090
1166
  if (err) {
1091
1167
  return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
1092
1168
  }
1093
- res.setHeader('Content-Length', compressed.length);
1094
- res.writeHead(res.statusCode || 200, res.getHeaders());
1169
+ if (!res.headersSent) {
1170
+ res.setHeader('Content-Encoding', 'deflate');
1171
+ res.setHeader('Content-Length', compressed.length);
1172
+ }
1095
1173
  res.end(compressed);
1096
1174
  });
1097
1175
  } else {
@@ -1519,13 +1597,15 @@ export const middleware = {
1519
1597
  // Only handle SSE requests
1520
1598
  if (req.headers.accept?.includes('text/event-stream')) {
1521
1599
  // Set SSE headers
1522
- res.writeHead(200, {
1523
- 'Content-Type': 'text/event-stream',
1524
- 'Cache-Control': 'no-cache',
1525
- Connection: 'keep-alive',
1526
- 'Access-Control-Allow-Origin': options.cors ? '*' : undefined,
1527
- 'Access-Control-Allow-Headers': options.cors ? 'Cache-Control' : undefined,
1528
- });
1600
+ if (!res.headersSent) {
1601
+ res.writeHead(200, {
1602
+ 'Content-Type': 'text/event-stream',
1603
+ 'Cache-Control': 'no-cache',
1604
+ Connection: 'keep-alive',
1605
+ 'Access-Control-Allow-Origin': options.cors ? '*' : undefined,
1606
+ 'Access-Control-Allow-Headers': options.cors ? 'Cache-Control' : undefined,
1607
+ });
1608
+ }
1529
1609
 
1530
1610
  // Add SSE methods to response
1531
1611
  (res as any).sendEvent = (data: any, event?: string, id?: string) => {
@@ -1626,7 +1706,8 @@ export const middleware = {
1626
1706
  const chunkSize = end - start + 1;
1627
1707
 
1628
1708
  if (start >= fileSize || end >= fileSize) {
1629
- res.status(416).setHeader('Content-Range', `bytes */${fileSize}`);
1709
+ res.status(416);
1710
+ res.setHeader('Content-Range', `bytes */${fileSize}`);
1630
1711
  res.json({ success: false, error: 'Range not satisfiable' });
1631
1712
  return;
1632
1713
  }
@@ -188,7 +188,9 @@ export const cache = (options: CacheOptions = {}): MiddlewareInterface => ({
188
188
  parts.push(`stale-while-revalidate=${directives.staleWhileRevalidate}`);
189
189
  }
190
190
 
191
- res.setHeader('Cache-Control', parts.join(', '));
191
+ if (!res.headersSent) {
192
+ res.setHeader('Cache-Control', parts.join(', '));
193
+ }
192
194
  return res;
193
195
  };
194
196
 
@@ -35,13 +35,15 @@ export const sse = (
35
35
  logger.debug('Setting up SSE connection', 'SSESetup');
36
36
 
37
37
  // Set SSE headers
38
- res.writeHead(200, {
39
- 'Content-Type': 'text/event-stream',
40
- 'Cache-Control': 'no-cache',
41
- Connection: 'keep-alive',
42
- 'Access-Control-Allow-Origin': options.cors ? '*' : undefined,
43
- 'Access-Control-Allow-Headers': options.cors ? 'Cache-Control' : undefined,
44
- });
38
+ if (!res.headersSent) {
39
+ res.writeHead(200, {
40
+ 'Content-Type': 'text/event-stream',
41
+ 'Cache-Control': 'no-cache',
42
+ Connection: 'keep-alive',
43
+ 'Access-Control-Allow-Origin': options.cors ? '*' : undefined,
44
+ 'Access-Control-Allow-Headers': options.cors ? 'Cache-Control' : undefined,
45
+ });
46
+ }
45
47
 
46
48
  // Add SSE methods to response
47
49
  res.sendEvent = (data: any, event?: string, id?: string) => {
@@ -24,6 +24,21 @@ export function defineModule(definition: ModuleDefinition): ModuleConfig {
24
24
  cache: route.cache,
25
25
  rateLimit: route.rateLimit,
26
26
  middleware: route.middleware,
27
+ // Copy all additional properties for extensibility
28
+ ...Object.fromEntries(
29
+ Object.entries(route).filter(
30
+ ([key]) =>
31
+ ![
32
+ 'method',
33
+ 'path',
34
+ 'handler',
35
+ 'validation',
36
+ 'cache',
37
+ 'rateLimit',
38
+ 'middleware',
39
+ ].includes(key)
40
+ )
41
+ ),
27
42
  }));
28
43
 
29
44
  // Store the actual route handler functions
@@ -138,24 +138,30 @@ export class NodeRuntimeAdapter extends BaseRuntimeAdapter {
138
138
 
139
139
  if (!enhanced.cookie) {
140
140
  enhanced.cookie = function (name: string, value: string, options?: any) {
141
- const cookieString = `${name}=${value}`;
142
- this.setHeader('Set-Cookie', cookieString);
141
+ if (!this.headersSent) {
142
+ const cookieString = `${name}=${value}`;
143
+ this.setHeader('Set-Cookie', cookieString);
144
+ }
143
145
  return this;
144
146
  };
145
147
  }
146
148
 
147
149
  if (!enhanced.clearCookie) {
148
150
  enhanced.clearCookie = function (name: string, options?: any) {
149
- this.setHeader('Set-Cookie', `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`);
151
+ if (!this.headersSent) {
152
+ this.setHeader('Set-Cookie', `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`);
153
+ }
150
154
  return this;
151
155
  };
152
156
  }
153
157
 
154
158
  if (!enhanced.redirect) {
155
159
  enhanced.redirect = function (url: string, status?: number) {
156
- this.statusCode = status || 302;
157
- this.setHeader('Location', url);
158
- this.end();
160
+ if (!this.headersSent) {
161
+ this.statusCode = status || 302;
162
+ this.setHeader('Location', url);
163
+ this.end();
164
+ }
159
165
  };
160
166
  }
161
167
 
package/src/types/http.ts CHANGED
@@ -22,9 +22,20 @@ export interface CookieOptions {
22
22
  sameSite?: 'strict' | 'lax' | 'none';
23
23
  domain?: string;
24
24
  path?: string;
25
+ // Security options
26
+ critical?: boolean; // Mark as critical for security (throws on late set)
27
+ throwOnLateSet?: boolean; // Force throw if headers already sent
25
28
  }
26
29
 
27
- export interface HttpResponse extends ServerResponse {
30
+ export interface ResponseState {
31
+ headersSent: boolean;
32
+ statusCode: number;
33
+ headers: Record<string, any>;
34
+ finished: boolean;
35
+ writable: boolean;
36
+ }
37
+
38
+ export interface MoroResponseMethods {
28
39
  json(data: any): void;
29
40
  status(code: number): HttpResponse;
30
41
  send(data: string | Buffer): void;
@@ -33,8 +44,19 @@ export interface HttpResponse extends ServerResponse {
33
44
  redirect(url: string, status?: number): void;
34
45
  sendFile(filePath: string): Promise<void>;
35
46
  render?(template: string, data?: any): Promise<void>;
47
+
48
+ // Header management utilities
49
+ hasHeader(name: string): boolean;
50
+ setBulkHeaders(headers: Record<string, string | number>): HttpResponse;
51
+ appendHeader(name: string, value: string | string[]): HttpResponse;
52
+
53
+ // Response state utilities
54
+ canSetHeaders(): boolean;
55
+ getResponseState(): ResponseState;
36
56
  }
37
57
 
58
+ export type HttpResponse = ServerResponse & MoroResponseMethods;
59
+
38
60
  export type HttpHandler = (req: HttpRequest, res: HttpResponse) => Promise<void> | void;
39
61
  export type Middleware = (
40
62
  req: HttpRequest,
@@ -7,6 +7,11 @@ export interface ModuleRoute {
7
7
  cache?: { ttl: number; key?: string };
8
8
  rateLimit?: { requests: number; window: number };
9
9
  middleware?: string[];
10
+ auth?: {
11
+ roles?: string[];
12
+ permissions?: string[];
13
+ optional?: boolean;
14
+ };
10
15
  }
11
16
 
12
17
  export interface ModuleSocket {
@@ -57,6 +62,13 @@ export interface InternalRouteDefinition {
57
62
  validation?: any;
58
63
  cache?: CacheConfig;
59
64
  rateLimit?: RateLimitConfig;
65
+ auth?: {
66
+ roles?: string[];
67
+ permissions?: string[];
68
+ optional?: boolean;
69
+ };
70
+ // Allow additional properties for extensibility
71
+ [key: string]: any;
60
72
  }
61
73
 
62
74
  export interface WebSocketDefinition {