@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.
- 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/framework.js +61 -0
- package/dist/core/framework.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/modules/modules.js +10 -0
- package/dist/core/modules/modules.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/dist/types/module.d.ts +11 -0
- 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/framework.ts +70 -0
- 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/modules/modules.ts +15 -0
- package/src/core/runtime/node-adapter.ts +12 -6
- package/src/types/http.ts +23 -1
- package/src/types/module.ts +12 -0
package/dist/types/http.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/types/module.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
package/src/core/framework.ts
CHANGED
|
@@ -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.
|
|
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) => {
|
|
@@ -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
|
-
|
|
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,
|
package/src/types/module.ts
CHANGED
|
@@ -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 {
|