@morojs/moro 1.5.6 → 1.5.8

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 (46) hide show
  1. package/dist/core/config/config-validator.js +7 -0
  2. package/dist/core/config/config-validator.js.map +1 -1
  3. package/dist/core/config/schema.js +7 -0
  4. package/dist/core/config/schema.js.map +1 -1
  5. package/dist/core/database/adapters/mongodb.d.ts +10 -0
  6. package/dist/core/database/adapters/mongodb.js +23 -2
  7. package/dist/core/database/adapters/mongodb.js.map +1 -1
  8. package/dist/core/database/adapters/mysql.d.ts +11 -0
  9. package/dist/core/database/adapters/mysql.js +1 -0
  10. package/dist/core/database/adapters/mysql.js.map +1 -1
  11. package/dist/core/database/adapters/postgresql.d.ts +9 -1
  12. package/dist/core/database/adapters/postgresql.js +1 -1
  13. package/dist/core/database/adapters/postgresql.js.map +1 -1
  14. package/dist/core/database/adapters/redis.d.ts +9 -0
  15. package/dist/core/database/adapters/redis.js +14 -4
  16. package/dist/core/database/adapters/redis.js.map +1 -1
  17. package/dist/core/framework.d.ts +2 -0
  18. package/dist/core/framework.js +34 -15
  19. package/dist/core/framework.js.map +1 -1
  20. package/dist/core/http/http-server.js +83 -14
  21. package/dist/core/http/http-server.js.map +1 -1
  22. package/dist/core/middleware/built-in/cache.js +3 -1
  23. package/dist/core/middleware/built-in/cache.js.map +1 -1
  24. package/dist/core/middleware/built-in/sse.js +9 -7
  25. package/dist/core/middleware/built-in/sse.js.map +1 -1
  26. package/dist/core/runtime/node-adapter.js +12 -6
  27. package/dist/core/runtime/node-adapter.js.map +1 -1
  28. package/dist/moro.js +18 -8
  29. package/dist/moro.js.map +1 -1
  30. package/dist/types/config.d.ts +7 -0
  31. package/dist/types/http.d.ts +16 -1
  32. package/package.json +1 -1
  33. package/src/core/config/config-validator.ts +7 -0
  34. package/src/core/config/schema.ts +7 -0
  35. package/src/core/database/adapters/mongodb.ts +30 -2
  36. package/src/core/database/adapters/mysql.ts +14 -0
  37. package/src/core/database/adapters/postgresql.ts +12 -2
  38. package/src/core/database/adapters/redis.ts +27 -4
  39. package/src/core/framework.ts +41 -15
  40. package/src/core/http/http-server.ts +95 -14
  41. package/src/core/middleware/built-in/cache.ts +3 -1
  42. package/src/core/middleware/built-in/sse.ts +9 -7
  43. package/src/core/runtime/node-adapter.ts +12 -6
  44. package/src/moro.ts +22 -8
  45. package/src/types/config.ts +7 -0
  46. package/src/types/http.ts +23 -1
@@ -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) => {
@@ -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/moro.ts CHANGED
@@ -99,6 +99,7 @@ export class Moro extends EventEmitter {
99
99
  ...options,
100
100
  logger: this.config.logging,
101
101
  websocket: this.config.websocket.enabled ? options.websocket || {} : false,
102
+ config: this.config,
102
103
  };
103
104
 
104
105
  this.coreFramework = new MoroCore(frameworkOptions);
@@ -855,20 +856,33 @@ export class Moro extends EventEmitter {
855
856
  }
856
857
 
857
858
  private setupDefaultMiddleware(options: MoroOptions) {
858
- // CORS
859
- if (options.cors !== false) {
860
- const corsOptions = typeof options.cors === 'object' ? options.cors : {};
859
+ // CORS - check config enabled property OR options.security.cors.enabled === true
860
+ if (this.config.security.cors.enabled || options.security?.cors?.enabled === true) {
861
+ const corsOptions =
862
+ typeof options.cors === 'object'
863
+ ? options.cors
864
+ : this.config.security.cors
865
+ ? this.config.security.cors
866
+ : {};
861
867
  this.use(middleware.cors(corsOptions));
862
868
  }
863
869
 
864
- // Helmet
865
- if (options.helmet !== false) {
870
+ // Helmet - check config enabled property OR options.security.helmet.enabled === true
871
+ if (this.config.security.helmet.enabled || options.security?.helmet?.enabled === true) {
866
872
  this.use(middleware.helmet());
867
873
  }
868
874
 
869
- // Compression
870
- if (options.compression !== false) {
871
- const compressionOptions = typeof options.compression === 'object' ? options.compression : {};
875
+ // Compression - check config enabled property OR options.performance.compression.enabled === true
876
+ if (
877
+ this.config.performance.compression.enabled ||
878
+ options.performance?.compression?.enabled === true
879
+ ) {
880
+ const compressionOptions =
881
+ typeof options.compression === 'object'
882
+ ? options.compression
883
+ : this.config.performance.compression
884
+ ? this.config.performance.compression
885
+ : {};
872
886
  this.use(middleware.compression(compressionOptions));
873
887
  }
874
888
 
@@ -5,6 +5,13 @@ export interface ServerConfig {
5
5
  host: string;
6
6
  maxConnections: number;
7
7
  timeout: number;
8
+ bodySizeLimit: string;
9
+ requestTracking: {
10
+ enabled: boolean;
11
+ };
12
+ errorBoundary: {
13
+ enabled: boolean;
14
+ };
8
15
  }
9
16
 
10
17
  export interface ServiceDiscoveryConfig {
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,