canxjs 1.0.1 → 1.1.0
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/README.md +13 -13
- package/dist/Application.d.ts +1 -1
- package/dist/Application.d.ts.map +1 -1
- package/dist/docs/ApiDoc.d.ts +3 -2
- package/dist/docs/ApiDoc.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +567 -203
- package/dist/middlewares/RateLimitMiddleware.d.ts +10 -0
- package/dist/middlewares/RateLimitMiddleware.d.ts.map +1 -0
- package/dist/middlewares/SecurityMiddleware.d.ts +14 -0
- package/dist/middlewares/SecurityMiddleware.d.ts.map +1 -0
- package/dist/middlewares/ValidationMiddleware.d.ts +4 -0
- package/dist/middlewares/ValidationMiddleware.d.ts.map +1 -0
- package/dist/schema/Schema.d.ts +64 -0
- package/dist/schema/Schema.d.ts.map +1 -0
- package/dist/utils/ErrorHandler.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/Application.ts +9 -2
- package/src/docs/ApiDoc.ts +12 -4
- package/src/index.ts +12 -0
- package/src/middlewares/RateLimitMiddleware.ts +62 -0
- package/src/middlewares/SecurityMiddleware.ts +55 -0
- package/src/middlewares/ValidationMiddleware.ts +35 -0
- package/src/schema/Schema.ts +299 -0
- package/src/utils/ErrorHandler.ts +45 -17
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MiddlewareHandler, CanxRequest } from '../types';
|
|
2
|
+
export interface RateLimitConfig {
|
|
3
|
+
windowMs: number;
|
|
4
|
+
max: number;
|
|
5
|
+
message?: string | object;
|
|
6
|
+
statusCode?: number;
|
|
7
|
+
keyGenerator?: (req: CanxRequest) => string;
|
|
8
|
+
}
|
|
9
|
+
export declare function rateLimit(config: RateLimitConfig): MiddlewareHandler;
|
|
10
|
+
//# sourceMappingURL=RateLimitMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RateLimitMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/RateLimitMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAgB,MAAM,UAAU,CAAC;AAE7E,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,CAAC;CAC7C;AAsBD,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CA+BpE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '../types';
|
|
2
|
+
export interface SecurityConfig {
|
|
3
|
+
xssProtection?: boolean;
|
|
4
|
+
contentTypeOptions?: boolean;
|
|
5
|
+
frameOptions?: 'DENY' | 'SAMEORIGIN';
|
|
6
|
+
hsts?: boolean | {
|
|
7
|
+
maxAge: number;
|
|
8
|
+
includeSubDomains: boolean;
|
|
9
|
+
};
|
|
10
|
+
contentSecurityPolicy?: string;
|
|
11
|
+
referrerPolicy?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function security(config?: SecurityConfig): MiddlewareHandler;
|
|
14
|
+
//# sourceMappingURL=SecurityMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/SecurityMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACrC,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,OAAO,CAAA;KAAE,CAAC;IAChE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,QAAQ,CAAC,MAAM,GAAE,cAAmB,GAAG,iBAAiB,CA2CvE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ValidationMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/ValidationMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG1C,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAE,MAAM,GAAG,OAAO,GAAG,QAAiB,GAAG,iBAAiB,CA8BnH"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ValidationError } from '../utils/ErrorHandler';
|
|
2
|
+
export type Infer<T extends Schema<any>> = T['_output'];
|
|
3
|
+
export interface ParseResult<T> {
|
|
4
|
+
success: boolean;
|
|
5
|
+
data?: T;
|
|
6
|
+
error?: ValidationError;
|
|
7
|
+
}
|
|
8
|
+
export declare abstract class Schema<Output = any, Input = unknown> {
|
|
9
|
+
readonly _output: Output;
|
|
10
|
+
readonly _input: Input;
|
|
11
|
+
protected description?: string;
|
|
12
|
+
protected isOptional: boolean;
|
|
13
|
+
abstract parse(value: unknown): Output;
|
|
14
|
+
abstract getJsonSchema(): Record<string, any>;
|
|
15
|
+
safeParse(value: unknown): ParseResult<Output>;
|
|
16
|
+
optional(): Schema<Output | undefined, Input | undefined>;
|
|
17
|
+
describe(description: string): this;
|
|
18
|
+
}
|
|
19
|
+
declare class StringSchema extends Schema<string> {
|
|
20
|
+
private checks;
|
|
21
|
+
constructor();
|
|
22
|
+
min(length: number, message?: string): this;
|
|
23
|
+
max(length: number, message?: string): this;
|
|
24
|
+
email(message?: string): this;
|
|
25
|
+
parse(value: unknown): string;
|
|
26
|
+
getJsonSchema(): Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
declare class NumberSchema extends Schema<number> {
|
|
29
|
+
private checks;
|
|
30
|
+
min(min: number, message?: string): this;
|
|
31
|
+
max(max: number, message?: string): this;
|
|
32
|
+
parse(value: unknown): number;
|
|
33
|
+
private validateChecks;
|
|
34
|
+
getJsonSchema(): Record<string, any>;
|
|
35
|
+
}
|
|
36
|
+
declare class BooleanSchema extends Schema<boolean> {
|
|
37
|
+
parse(value: unknown): boolean;
|
|
38
|
+
getJsonSchema(): Record<string, any>;
|
|
39
|
+
}
|
|
40
|
+
declare class ObjectSchema<T extends Record<string, Schema<any>>> extends Schema<{
|
|
41
|
+
[K in keyof T]: Infer<T[K]>;
|
|
42
|
+
}> {
|
|
43
|
+
private shape;
|
|
44
|
+
constructor(shape: T);
|
|
45
|
+
parse(value: unknown): {
|
|
46
|
+
[K in keyof T]: Infer<T[K]>;
|
|
47
|
+
};
|
|
48
|
+
getJsonSchema(): Record<string, any>;
|
|
49
|
+
}
|
|
50
|
+
declare class ArraySchema<T extends Schema<any>> extends Schema<Infer<T>[]> {
|
|
51
|
+
private element;
|
|
52
|
+
constructor(element: T);
|
|
53
|
+
parse(value: unknown): Infer<T>[];
|
|
54
|
+
getJsonSchema(): Record<string, any>;
|
|
55
|
+
}
|
|
56
|
+
export declare const z: {
|
|
57
|
+
string: () => StringSchema;
|
|
58
|
+
number: () => NumberSchema;
|
|
59
|
+
boolean: () => BooleanSchema;
|
|
60
|
+
object: <T extends Record<string, Schema<any>>>(shape: T) => ObjectSchema<T>;
|
|
61
|
+
array: <T extends Schema<any>>(element: T) => ArraySchema<T>;
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=Schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../../src/schema/Schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorHandler.d.ts","sourceRoot":"","sources":["../../src/utils/ErrorHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,qBAAa,SAAU,SAAQ,KAAK;IAClC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClD,SAAgB,SAAS,EAAE,IAAI,CAAC;gBAG9B,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAAqB,EAC3B,UAAU,GAAE,MAAY,EACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAWnC,MAAM;;;;;;;;CAUP;AAED,qBAAa,eAAgB,SAAQ,SAAS;IAC5C,SAAgB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;gBAElC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,SAAsB;CAMpG;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,QAAQ,GAAE,MAAmB,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;CAKhE;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,SAA4B;CAIhD;AAED,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,SAAuD;CAI3E;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAsB,EAAE,QAAQ,CAAC,EAAE,MAAM;CAI7D;AAED,qBAAa,cAAe,SAAQ,SAAS;IAC3C,SAAgB,UAAU,EAAE,MAAM,CAAC;gBAEvB,UAAU,GAAE,MAAW,EAAE,OAAO,SAAsB;CAKnE;AAED,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,SAAgB;CAIpC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAmB,EAAE,aAAa,CAAC,EAAE,KAAK;CAO9D;AAED,qBAAa,uBAAwB,SAAQ,SAAS;gBACxC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAI9C;AAMD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7E,UAAU,mBAAmB;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,KAAK,IAAI,CAAC;CACpD;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,iBAAiB,
|
|
1
|
+
{"version":3,"file":"ErrorHandler.d.ts","sourceRoot":"","sources":["../../src/utils/ErrorHandler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canxjs",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Ultra-fast async-first MVC backend framework for Bun runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -93,5 +93,8 @@
|
|
|
93
93
|
"bugs": {
|
|
94
94
|
"url": "https://github.com/chandafa/canx.JS/issues"
|
|
95
95
|
},
|
|
96
|
-
"homepage": "https://github.com/chandafa/canx.JS#readme"
|
|
96
|
+
"homepage": "https://github.com/chandafa/canx.JS#readme",
|
|
97
|
+
"publishConfig": {
|
|
98
|
+
"access": "public"
|
|
99
|
+
}
|
|
97
100
|
}
|
package/src/Application.ts
CHANGED
|
@@ -76,8 +76,15 @@ export class Canx implements CanxApplication {
|
|
|
76
76
|
/**
|
|
77
77
|
* Start the server
|
|
78
78
|
*/
|
|
79
|
-
async listen(port?: number, callback?: () => void): Promise<void> {
|
|
80
|
-
if (
|
|
79
|
+
async listen(port?: number | (() => void), callback?: () => void): Promise<void> {
|
|
80
|
+
if (typeof port === 'function') {
|
|
81
|
+
callback = port;
|
|
82
|
+
port = undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (port && typeof port === 'number') {
|
|
86
|
+
this.config.port = port;
|
|
87
|
+
}
|
|
81
88
|
|
|
82
89
|
// Initialize plugins
|
|
83
90
|
for (const plugin of this.plugins) {
|
package/src/docs/ApiDoc.ts
CHANGED
|
@@ -77,25 +77,33 @@ export function ApiParam(param: ApiParameter): MethodDecorator {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
import { Schema } from '../schema/Schema';
|
|
81
|
+
|
|
82
|
+
export function ApiBody(schema: ApiSchema | Schema<any>, required = true): MethodDecorator {
|
|
81
83
|
return (target: any, propertyKey: string | symbol) => {
|
|
82
84
|
if (!apiMetadata.has(target)) apiMetadata.set(target, new Map());
|
|
83
85
|
const meta = apiMetadata.get(target)!;
|
|
84
86
|
const existing = meta.get(String(propertyKey)) || {} as ApiEndpoint;
|
|
85
|
-
|
|
87
|
+
|
|
88
|
+
const finalSchema = schema instanceof Schema ? schema.getJsonSchema() : schema;
|
|
89
|
+
|
|
90
|
+
existing.requestBody = { required, content: { 'application/json': { schema: finalSchema as ApiSchema } } };
|
|
86
91
|
meta.set(String(propertyKey), existing);
|
|
87
92
|
};
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
export function ApiResponse(status: number, description: string, schema?: ApiSchema): MethodDecorator {
|
|
95
|
+
export function ApiResponse(status: number, description: string, schema?: ApiSchema | Schema<any>): MethodDecorator {
|
|
91
96
|
return (target: any, propertyKey: string | symbol) => {
|
|
92
97
|
if (!apiMetadata.has(target)) apiMetadata.set(target, new Map());
|
|
93
98
|
const meta = apiMetadata.get(target)!;
|
|
94
99
|
const existing = meta.get(String(propertyKey)) || {} as ApiEndpoint;
|
|
95
100
|
existing.responses = existing.responses || {};
|
|
101
|
+
|
|
102
|
+
const finalSchema = schema instanceof Schema ? schema.getJsonSchema() : schema;
|
|
103
|
+
|
|
96
104
|
existing.responses[String(status)] = {
|
|
97
105
|
description,
|
|
98
|
-
content:
|
|
106
|
+
content: finalSchema ? { 'application/json': { schema: finalSchema as ApiSchema } } : undefined,
|
|
99
107
|
};
|
|
100
108
|
meta.set(String(propertyKey), existing);
|
|
101
109
|
};
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,19 @@
|
|
|
9
9
|
// ============================================
|
|
10
10
|
export { Server, createCanxRequest, createCanxResponse } from './core/Server';
|
|
11
11
|
export { Router, createRouter } from './core/Router';
|
|
12
|
+
// ============================================
|
|
13
|
+
// Middleware Exports
|
|
14
|
+
// ============================================
|
|
12
15
|
export { MiddlewarePipeline, cors, logger, bodyParser, rateLimit, compress, createMiddlewarePipeline } from './core/Middleware';
|
|
16
|
+
export { security } from './middlewares/SecurityMiddleware';
|
|
17
|
+
export { rateLimit as rateLimitMiddleware } from './middlewares/RateLimitMiddleware';
|
|
18
|
+
export { validateSchema } from './middlewares/ValidationMiddleware';
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// Schema / Validation Exports
|
|
22
|
+
// ============================================
|
|
23
|
+
export { z, Schema as ZSchema } from './schema/Schema';
|
|
24
|
+
export type { Infer } from './schema/Schema';
|
|
13
25
|
|
|
14
26
|
// ============================================
|
|
15
27
|
// MVC Exports
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MiddlewareHandler, CanxRequest, CanxResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface RateLimitConfig {
|
|
4
|
+
windowMs: number;
|
|
5
|
+
max: number;
|
|
6
|
+
message?: string | object;
|
|
7
|
+
statusCode?: number;
|
|
8
|
+
keyGenerator?: (req: CanxRequest) => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Simple in-memory store. In production for multiple instances, Redis is recommended.
|
|
12
|
+
// This is exposed so it can be swapped if needed.
|
|
13
|
+
class MemoryStore {
|
|
14
|
+
hits = new Map<string, { count: number; resetTime: number }>();
|
|
15
|
+
|
|
16
|
+
increment(key: string, windowMs: number): { count: number; resetTime: number } {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
let record = this.hits.get(key);
|
|
19
|
+
|
|
20
|
+
if (!record || record.resetTime <= now) {
|
|
21
|
+
record = { count: 1, resetTime: now + windowMs };
|
|
22
|
+
} else {
|
|
23
|
+
record.count++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.hits.set(key, record);
|
|
27
|
+
return record;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function rateLimit(config: RateLimitConfig): MiddlewareHandler {
|
|
32
|
+
const store = new MemoryStore();
|
|
33
|
+
const {
|
|
34
|
+
windowMs = 60000,
|
|
35
|
+
max = 100,
|
|
36
|
+
message = 'Too many requests, please try again later.',
|
|
37
|
+
statusCode = 429,
|
|
38
|
+
keyGenerator = (req: CanxRequest) => req.header('x-forwarded-for') || req.header('cf-connecting-ip') || 'unknown-ip'
|
|
39
|
+
} = config;
|
|
40
|
+
|
|
41
|
+
return async (req, res, next) => {
|
|
42
|
+
const key = keyGenerator(req);
|
|
43
|
+
const { count, resetTime } = store.increment(key, windowMs);
|
|
44
|
+
|
|
45
|
+
const remaining = Math.max(0, max - count);
|
|
46
|
+
const resetDate = new Date(resetTime);
|
|
47
|
+
|
|
48
|
+
// Standard Rate Limit Headers
|
|
49
|
+
res.header('X-RateLimit-Limit', String(max));
|
|
50
|
+
res.header('X-RateLimit-Remaining', String(remaining));
|
|
51
|
+
res.header('X-RateLimit-Reset', String(Math.ceil(resetTime / 1000)));
|
|
52
|
+
|
|
53
|
+
if (count > max) {
|
|
54
|
+
if (typeof message === 'object') {
|
|
55
|
+
return res.status(statusCode).json(message);
|
|
56
|
+
}
|
|
57
|
+
return res.status(statusCode).text(String(message));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return next();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface SecurityConfig {
|
|
4
|
+
xssProtection?: boolean;
|
|
5
|
+
contentTypeOptions?: boolean;
|
|
6
|
+
frameOptions?: 'DENY' | 'SAMEORIGIN';
|
|
7
|
+
hsts?: boolean | { maxAge: number; includeSubDomains: boolean };
|
|
8
|
+
contentSecurityPolicy?: string;
|
|
9
|
+
referrerPolicy?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function security(config: SecurityConfig = {}): MiddlewareHandler {
|
|
13
|
+
return async (req, res, next) => {
|
|
14
|
+
// X-XSS-Protection
|
|
15
|
+
if (config.xssProtection !== false) {
|
|
16
|
+
res.header('X-XSS-Protection', '1; mode=block');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// X-Content-Type-Options
|
|
20
|
+
if (config.contentTypeOptions !== false) {
|
|
21
|
+
res.header('X-Content-Type-Options', 'nosniff');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// X-Frame-Options
|
|
25
|
+
if (config.frameOptions) {
|
|
26
|
+
res.header('X-Frame-Options', config.frameOptions);
|
|
27
|
+
} else if (config.frameOptions !== undefined) {
|
|
28
|
+
// Allow if explicit string (e.g. ALLOW-FROM) - though deprecated
|
|
29
|
+
res.header('X-Frame-Options', config.frameOptions);
|
|
30
|
+
} else {
|
|
31
|
+
// Default deny if not specified? Or standard default?
|
|
32
|
+
// Modern default is usually SAMEORIGIN or DENY. Let's do SAMEORIGIN.
|
|
33
|
+
res.header('X-Frame-Options', 'SAMEORIGIN');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Strict-Transport-Security
|
|
37
|
+
if (config.hsts) {
|
|
38
|
+
const maxAge = typeof config.hsts === 'object' ? config.hsts.maxAge : 31536000;
|
|
39
|
+
const includeSubDomains = typeof config.hsts === 'object' && config.hsts.includeSubDomains ? '; includeSubDomains' : '';
|
|
40
|
+
res.header('Strict-Transport-Security', `max-age=${maxAge}${includeSubDomains}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Content-Security-Policy
|
|
44
|
+
if (config.contentSecurityPolicy) {
|
|
45
|
+
res.header('Content-Security-Policy', config.contentSecurityPolicy);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Referrer-Policy
|
|
49
|
+
if (config.referrerPolicy) {
|
|
50
|
+
res.header('Referrer-Policy', config.referrerPolicy);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return next();
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MiddlewareHandler, CanxRequest } from '../types';
|
|
2
|
+
import { Schema } from '../schema/Schema';
|
|
3
|
+
import { ValidationError } from '../utils/ErrorHandler';
|
|
4
|
+
|
|
5
|
+
export function validateSchema(schema: Schema<any>, target: 'body' | 'query' | 'params' = 'body'): MiddlewareHandler {
|
|
6
|
+
return async (req: CanxRequest, res, next) => {
|
|
7
|
+
let data: unknown;
|
|
8
|
+
|
|
9
|
+
if (target === 'body') {
|
|
10
|
+
data = await req.body();
|
|
11
|
+
} else if (target === 'query') {
|
|
12
|
+
data = req.query;
|
|
13
|
+
} else {
|
|
14
|
+
data = req.params;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const validated = schema.parse(data);
|
|
19
|
+
// Attach validated data to request for type safety in controllers
|
|
20
|
+
(req as any).validatedData = validated;
|
|
21
|
+
return next();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof ValidationError) {
|
|
24
|
+
return res.status(422).json({
|
|
25
|
+
error: {
|
|
26
|
+
code: 'VALIDATION_ERROR',
|
|
27
|
+
message: 'Validation failed',
|
|
28
|
+
details: Object.fromEntries(error.errors),
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { ValidationError } from '../utils/ErrorHandler';
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// Core Types
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
export type Infer<T extends Schema<any>> = T['_output'];
|
|
8
|
+
|
|
9
|
+
export interface ParseResult<T> {
|
|
10
|
+
success: boolean;
|
|
11
|
+
data?: T;
|
|
12
|
+
error?: ValidationError;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export abstract class Schema<Output = any, Input = unknown> {
|
|
16
|
+
readonly _output!: Output;
|
|
17
|
+
readonly _input!: Input;
|
|
18
|
+
protected description?: string;
|
|
19
|
+
protected isOptional: boolean = false;
|
|
20
|
+
|
|
21
|
+
abstract parse(value: unknown): Output;
|
|
22
|
+
abstract getJsonSchema(): Record<string, any>;
|
|
23
|
+
|
|
24
|
+
safeParse(value: unknown): ParseResult<Output> {
|
|
25
|
+
try {
|
|
26
|
+
const data = this.parse(value);
|
|
27
|
+
return { success: true, data };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof ValidationError) {
|
|
30
|
+
return { success: false, error };
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
optional(): Schema<Output | undefined, Input | undefined> {
|
|
37
|
+
const newSchema = Object.create(this);
|
|
38
|
+
newSchema.isOptional = true;
|
|
39
|
+
return newSchema as any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe(description: string): this {
|
|
43
|
+
this.description = description;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================
|
|
49
|
+
// String Schema
|
|
50
|
+
// ============================================
|
|
51
|
+
|
|
52
|
+
class StringSchema extends Schema<string> {
|
|
53
|
+
private checks: Array<(v: string) => string | null> = [];
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
super();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
min(length: number, message?: string): this {
|
|
60
|
+
this.checks.push((v) => v.length >= length ? null : (message || `String must contain at least ${length} character(s)`));
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
max(length: number, message?: string): this {
|
|
65
|
+
this.checks.push((v) => v.length <= length ? null : (message || `String must contain at most ${length} character(s)`));
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
email(message?: string): this {
|
|
70
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
71
|
+
this.checks.push((v) => emailRegex.test(v) ? null : (message || 'Invalid email address'));
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
parse(value: unknown): string {
|
|
76
|
+
if (this.isOptional && (value === undefined || value === null)) {
|
|
77
|
+
return value as any;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof value !== 'string') {
|
|
81
|
+
throw new ValidationError({ _errors: ['Expected string, received ' + typeof value] });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const check of this.checks) {
|
|
85
|
+
const error = check(value);
|
|
86
|
+
if (error) {
|
|
87
|
+
throw new ValidationError({ _errors: [error] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getJsonSchema(): Record<string, any> {
|
|
95
|
+
return {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: this.description,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// Number Schema
|
|
104
|
+
// ============================================
|
|
105
|
+
|
|
106
|
+
class NumberSchema extends Schema<number> {
|
|
107
|
+
private checks: Array<(v: number) => string | null> = [];
|
|
108
|
+
|
|
109
|
+
min(min: number, message?: string): this {
|
|
110
|
+
this.checks.push((v) => v >= min ? null : (message || `Number must be greater than or equal to ${min}`));
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
max(max: number, message?: string): this {
|
|
115
|
+
this.checks.push((v) => v <= max ? null : (message || `Number must be less than or equal to ${max}`));
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
parse(value: unknown): number {
|
|
120
|
+
if (this.isOptional && (value === undefined || value === null)) {
|
|
121
|
+
return value as any;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
125
|
+
// Try converting string number
|
|
126
|
+
if (typeof value === 'string' && !isNaN(parseFloat(value))) {
|
|
127
|
+
const num = parseFloat(value);
|
|
128
|
+
return this.validateChecks(num);
|
|
129
|
+
}
|
|
130
|
+
throw new ValidationError({ _errors: ['Expected number, received ' + typeof value] });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return this.validateChecks(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private validateChecks(value: number): number {
|
|
137
|
+
for (const check of this.checks) {
|
|
138
|
+
const error = check(value);
|
|
139
|
+
if (error) {
|
|
140
|
+
throw new ValidationError({ _errors: [error] });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getJsonSchema(): Record<string, any> {
|
|
147
|
+
return {
|
|
148
|
+
type: 'number',
|
|
149
|
+
description: this.description,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================
|
|
155
|
+
// Boolean Schema
|
|
156
|
+
// ============================================
|
|
157
|
+
|
|
158
|
+
class BooleanSchema extends Schema<boolean> {
|
|
159
|
+
parse(value: unknown): boolean {
|
|
160
|
+
if (this.isOptional && (value === undefined || value === null)) {
|
|
161
|
+
return value as any;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof value === 'boolean') return value;
|
|
165
|
+
if (value === 'true') return true;
|
|
166
|
+
if (value === 'false') return false;
|
|
167
|
+
|
|
168
|
+
throw new ValidationError({ _errors: ['Expected boolean, received ' + typeof value] });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getJsonSchema(): Record<string, any> {
|
|
172
|
+
return {
|
|
173
|
+
type: 'boolean',
|
|
174
|
+
description: this.description,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================
|
|
180
|
+
// Object Schema
|
|
181
|
+
// ============================================
|
|
182
|
+
|
|
183
|
+
class ObjectSchema<T extends Record<string, Schema<any>>> extends Schema<{ [K in keyof T]: Infer<T[K]> }> {
|
|
184
|
+
constructor(private shape: T) {
|
|
185
|
+
super();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
parse(value: unknown): { [K in keyof T]: Infer<T[K]> } {
|
|
189
|
+
if (this.isOptional && (value === undefined || value === null)) {
|
|
190
|
+
return value as any;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
194
|
+
throw new ValidationError({ _errors: ['Expected object, received ' + typeof value] });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result: any = {};
|
|
198
|
+
const errors = new Map<string, string[]>();
|
|
199
|
+
|
|
200
|
+
for (const [key, schema] of Object.entries(this.shape)) {
|
|
201
|
+
try {
|
|
202
|
+
result[key] = schema.parse((value as any)[key]);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof ValidationError) {
|
|
205
|
+
const fieldErrors = error.errors.get('_errors') || [];
|
|
206
|
+
// If the child error has map errors (nested object), merge them
|
|
207
|
+
if (error.errors.size > 0 && !error.errors.has('_errors')) {
|
|
208
|
+
error.errors.forEach((msgs, path) => {
|
|
209
|
+
errors.set(`${key}.${path}`, msgs);
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
errors.set(key, fieldErrors);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
errors.set(key, ['Invalid value']);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (errors.size > 0) {
|
|
221
|
+
throw new ValidationError(errors);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getJsonSchema(): Record<string, any> {
|
|
228
|
+
const properties: Record<string, any> = {};
|
|
229
|
+
const required: string[] = [];
|
|
230
|
+
|
|
231
|
+
for (const [key, schema] of Object.entries(this.shape)) {
|
|
232
|
+
properties[key] = schema.getJsonSchema();
|
|
233
|
+
if (!(schema as any).isOptional) {
|
|
234
|
+
// Actually we can check isOptional property
|
|
235
|
+
if (!(schema as any).isOptional) {
|
|
236
|
+
required.push(key);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties,
|
|
244
|
+
required: required.length > 0 ? required : undefined,
|
|
245
|
+
description: this.description,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================
|
|
251
|
+
// Array Schema
|
|
252
|
+
// ============================================
|
|
253
|
+
|
|
254
|
+
class ArraySchema<T extends Schema<any>> extends Schema<Infer<T>[]> {
|
|
255
|
+
constructor(private element: T) {
|
|
256
|
+
super();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
parse(value: unknown): Infer<T>[] {
|
|
260
|
+
if (this.isOptional && (value === undefined || value === null)) {
|
|
261
|
+
return value as any;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!Array.isArray(value)) {
|
|
265
|
+
throw new ValidationError({ _errors: ['Expected array, received ' + typeof value] });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return value.map((item, index) => {
|
|
269
|
+
try {
|
|
270
|
+
return this.element.parse(item);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error instanceof ValidationError) {
|
|
273
|
+
throw new ValidationError({ [index]: error.errors.get('_errors') || ['Invalid Item'] });
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getJsonSchema(): Record<string, any> {
|
|
281
|
+
return {
|
|
282
|
+
type: 'array',
|
|
283
|
+
items: this.element.getJsonSchema(),
|
|
284
|
+
description: this.description,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ============================================
|
|
290
|
+
// Builder
|
|
291
|
+
// ============================================
|
|
292
|
+
|
|
293
|
+
export const z = {
|
|
294
|
+
string: () => new StringSchema(),
|
|
295
|
+
number: () => new NumberSchema(),
|
|
296
|
+
boolean: () => new BooleanSchema(),
|
|
297
|
+
object: <T extends Record<string, Schema<any>>>(shape: T) => new ObjectSchema(shape),
|
|
298
|
+
array: <T extends Schema<any>>(element: T) => new ArraySchema(element),
|
|
299
|
+
};
|