@morojs/moro 1.5.7 → 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.
- package/dist/core/database/adapters/mongodb.d.ts +10 -0
- package/dist/core/database/adapters/mongodb.js +23 -2
- package/dist/core/database/adapters/mongodb.js.map +1 -1
- package/dist/core/database/adapters/mysql.d.ts +11 -0
- package/dist/core/database/adapters/mysql.js +1 -0
- package/dist/core/database/adapters/mysql.js.map +1 -1
- package/dist/core/database/adapters/postgresql.d.ts +9 -1
- package/dist/core/database/adapters/postgresql.js +1 -1
- package/dist/core/database/adapters/postgresql.js.map +1 -1
- package/dist/core/database/adapters/redis.d.ts +9 -0
- package/dist/core/database/adapters/redis.js +14 -4
- package/dist/core/database/adapters/redis.js.map +1 -1
- package/dist/core/http/http-server.js +83 -14
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/middleware/built-in/cache.js +3 -1
- package/dist/core/middleware/built-in/cache.js.map +1 -1
- package/dist/core/middleware/built-in/sse.js +9 -7
- package/dist/core/middleware/built-in/sse.js.map +1 -1
- package/dist/core/runtime/node-adapter.js +12 -6
- package/dist/core/runtime/node-adapter.js.map +1 -1
- package/dist/types/http.d.ts +16 -1
- package/package.json +1 -1
- package/src/core/database/adapters/mongodb.ts +30 -2
- package/src/core/database/adapters/mysql.ts +14 -0
- package/src/core/database/adapters/postgresql.ts +12 -2
- package/src/core/database/adapters/redis.ts +27 -4
- package/src/core/http/http-server.ts +95 -14
- package/src/core/middleware/built-in/cache.ts +3 -1
- package/src/core/middleware/built-in/sse.ts +9 -7
- package/src/core/runtime/node-adapter.ts +12 -6
- 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.
|
|
1084
|
-
|
|
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.
|
|
1094
|
-
|
|
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.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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)
|
|
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.
|
|
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.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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.
|
|
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.
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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,
|