azurajs 3.0.1 → 3.0.2

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 (106) hide show
  1. package/dist/config/index.js +128 -6
  2. package/dist/config/index.js.map +1 -1
  3. package/dist/config/index.mjs +130 -1
  4. package/dist/config/index.mjs.map +1 -1
  5. package/dist/core/index.js +1100 -11
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +1102 -3
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/decorators/index.js +117 -87
  10. package/dist/decorators/index.js.map +1 -1
  11. package/dist/decorators/index.mjs +98 -1
  12. package/dist/decorators/index.mjs.map +1 -1
  13. package/dist/index.js +2592 -236
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +2537 -9
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/middleware/index.js +16 -7
  18. package/dist/middleware/index.js.map +1 -1
  19. package/dist/middleware/index.mjs +17 -1
  20. package/dist/middleware/index.mjs.map +1 -1
  21. package/dist/plugins/index.js +1056 -73
  22. package/dist/plugins/index.js.map +1 -1
  23. package/dist/plugins/index.mjs +1042 -1
  24. package/dist/plugins/index.mjs.map +1 -1
  25. package/dist/types/index.js +49 -12
  26. package/dist/types/index.js.map +1 -1
  27. package/dist/types/index.mjs +49 -2
  28. package/dist/types/index.mjs.map +1 -1
  29. package/dist/utils/index.js +551 -50
  30. package/dist/utils/index.js.map +1 -1
  31. package/dist/utils/index.mjs +541 -3
  32. package/dist/utils/index.mjs.map +1 -1
  33. package/package.json +35 -17
  34. package/{dist/chunk-DR254CWJ.mjs → src/config/ConfigModule.ts} +169 -132
  35. package/src/config/index.ts +1 -0
  36. package/src/core/index.ts +2 -0
  37. package/src/core/router.ts +284 -0
  38. package/{dist/chunk-EYAHUNC7.mjs → src/core/server.ts} +590 -699
  39. package/src/decorators/Route.ts +110 -0
  40. package/src/decorators/index.ts +23 -0
  41. package/src/index.ts +12 -0
  42. package/src/middleware/LoggingMiddleware.ts +20 -0
  43. package/src/middleware/index.ts +1 -0
  44. package/src/plugins/CORSPlugin.ts +56 -0
  45. package/src/plugins/CircuitBreakerPlugin.ts +84 -0
  46. package/src/plugins/CompressionPlugin.ts +80 -0
  47. package/src/plugins/ETagPlugin.ts +31 -0
  48. package/src/plugins/HealthCheckPlugin.ts +57 -0
  49. package/src/plugins/HelmetPlugin.ts +89 -0
  50. package/src/plugins/JWTPlugin.ts +132 -0
  51. package/src/plugins/MultipartPlugin.ts +168 -0
  52. package/src/plugins/ProxyPlugin.ts +89 -0
  53. package/src/plugins/RateLimitPlugin.ts +96 -0
  54. package/src/plugins/RequestIdPlugin.ts +21 -0
  55. package/src/plugins/SSEPlugin.ts +114 -0
  56. package/src/plugins/SessionPlugin.ts +98 -0
  57. package/src/plugins/StaticPlugin.ts +152 -0
  58. package/src/plugins/TimeoutPlugin.ts +33 -0
  59. package/src/plugins/index.ts +18 -0
  60. package/src/types/common.type.ts +82 -0
  61. package/src/types/config.type.ts +57 -0
  62. package/{dist/chunk-OWUGAI5V.mjs → src/types/http/status.ts} +49 -51
  63. package/src/types/index.ts +55 -0
  64. package/src/types/plugins/plugin.type.ts +170 -0
  65. package/src/types/reflect.d.ts +14 -0
  66. package/src/types/routes.type.ts +70 -0
  67. package/src/utils/HttpError.ts +62 -0
  68. package/src/utils/IpResolver.ts +30 -0
  69. package/src/utils/Logger.ts +144 -0
  70. package/src/utils/Parser.ts +182 -0
  71. package/src/utils/cookies/CookieManager.ts +48 -0
  72. package/src/utils/index.ts +9 -0
  73. package/{dist/chunk-UWIFSGSQ.mjs → src/utils/validators/DTOValidator.ts} +145 -141
  74. package/src/utils/validators/SchemaValidator.ts +45 -0
  75. package/dist/chunk-3UFAWS2V.js +0 -392
  76. package/dist/chunk-3UFAWS2V.js.map +0 -1
  77. package/dist/chunk-4LSFAAZW.js +0 -4
  78. package/dist/chunk-4LSFAAZW.js.map +0 -1
  79. package/dist/chunk-7NSRIVZM.js +0 -54
  80. package/dist/chunk-7NSRIVZM.js.map +0 -1
  81. package/dist/chunk-AOG6NYAM.js +0 -144
  82. package/dist/chunk-AOG6NYAM.js.map +0 -1
  83. package/dist/chunk-DR254CWJ.mjs.map +0 -1
  84. package/dist/chunk-EYAHUNC7.mjs.map +0 -1
  85. package/dist/chunk-HHDQPIJN.mjs +0 -19
  86. package/dist/chunk-HHDQPIJN.mjs.map +0 -1
  87. package/dist/chunk-HHZNAGGI.js +0 -702
  88. package/dist/chunk-HHZNAGGI.js.map +0 -1
  89. package/dist/chunk-KJM5XCAY.js +0 -21
  90. package/dist/chunk-KJM5XCAY.js.map +0 -1
  91. package/dist/chunk-NLSZKAPA.mjs +0 -1044
  92. package/dist/chunk-NLSZKAPA.mjs.map +0 -1
  93. package/dist/chunk-OWUGAI5V.mjs.map +0 -1
  94. package/dist/chunk-POPNQEOK.js +0 -1063
  95. package/dist/chunk-POPNQEOK.js.map +0 -1
  96. package/dist/chunk-QPRW4YU4.js +0 -134
  97. package/dist/chunk-QPRW4YU4.js.map +0 -1
  98. package/dist/chunk-REJDZUZ5.mjs +0 -382
  99. package/dist/chunk-REJDZUZ5.mjs.map +0 -1
  100. package/dist/chunk-TC6N6TJZ.mjs +0 -100
  101. package/dist/chunk-TC6N6TJZ.mjs.map +0 -1
  102. package/dist/chunk-TEUXKMXP.js +0 -122
  103. package/dist/chunk-TEUXKMXP.js.map +0 -1
  104. package/dist/chunk-UWIFSGSQ.mjs.map +0 -1
  105. package/dist/chunk-YPBKY4KY.mjs +0 -3
  106. package/dist/chunk-YPBKY4KY.mjs.map +0 -1
@@ -0,0 +1,170 @@
1
+ import type {
2
+ AzuraRequest,
3
+ AzuraResponse,
4
+ NextFunction,
5
+ } from "../common.type.js";
6
+
7
+ export interface PluginContext {
8
+ req: AzuraRequest;
9
+ res: AzuraResponse;
10
+ }
11
+
12
+ export type PluginHandler = (
13
+ ctx: PluginContext,
14
+ next: NextFunction,
15
+ ) => void | Promise<void>;
16
+
17
+ export type PluginFactory<T = any> = (options?: T) => PluginHandler;
18
+
19
+ export interface PluginDefinition<T = any> {
20
+ name: string;
21
+ version?: string;
22
+ factory: PluginFactory<T>;
23
+ defaultOptions?: T;
24
+ }
25
+
26
+ export interface CORSOptions {
27
+ origins?: string | string[] | ((origin: string) => boolean);
28
+ methods?: string[];
29
+ allowedHeaders?: string[];
30
+ exposedHeaders?: string[];
31
+ credentials?: boolean;
32
+ maxAge?: number;
33
+ preflightContinue?: boolean;
34
+ }
35
+
36
+ export interface RateLimitOptions {
37
+ windowMs?: number;
38
+ max?: number;
39
+ message?: string | object;
40
+ statusCode?: number;
41
+ keyGenerator?: (req: AzuraRequest) => string;
42
+ skipSuccessfulRequests?: boolean;
43
+ skipFailedRequests?: boolean;
44
+ store?: RateLimitStore;
45
+ }
46
+
47
+ export interface RateLimitStore {
48
+ increment(key: string): Promise<{ totalHits: number; resetTime: Date }>;
49
+ decrement(key: string): Promise<void>;
50
+ resetKey(key: string): Promise<void>;
51
+ }
52
+
53
+ export interface HelmetOptions {
54
+ contentSecurityPolicy?: false | Record<string, string[]>;
55
+ crossOriginEmbedderPolicy?: boolean;
56
+ crossOriginOpenerPolicy?: boolean | { policy: string };
57
+ crossOriginResourcePolicy?: boolean | { policy: string };
58
+ dnsPrefetchControl?: boolean | { allow: boolean };
59
+ frameguard?: boolean | { action: "deny" | "sameorigin" };
60
+ hsts?: boolean | { maxAge: number; includeSubDomains?: boolean; preload?: boolean };
61
+ ieNoOpen?: boolean;
62
+ noSniff?: boolean;
63
+ referrerPolicy?: boolean | { policy: string | string[] };
64
+ xssFilter?: boolean;
65
+ permissionsPolicy?: Record<string, string[]>;
66
+ }
67
+
68
+ export interface JWTOptions {
69
+ secret: string;
70
+ algorithm?: "HS256" | "HS384" | "HS512";
71
+ expiresIn?: string | number;
72
+ issuer?: string;
73
+ audience?: string;
74
+ paths?: string[];
75
+ exclude?: string[];
76
+ getToken?: (req: AzuraRequest) => string | null;
77
+ }
78
+
79
+ export interface SessionOptions {
80
+ secret: string;
81
+ name?: string;
82
+ maxAge?: number;
83
+ secure?: boolean;
84
+ httpOnly?: boolean;
85
+ sameSite?: "Strict" | "Lax" | "None";
86
+ store?: SessionStore;
87
+ }
88
+
89
+ export interface SessionStore {
90
+ get(id: string): Promise<Record<string, any> | null>;
91
+ set(id: string, data: Record<string, any>, maxAge: number): Promise<void>;
92
+ destroy(id: string): Promise<void>;
93
+ touch?(id: string, maxAge: number): Promise<void>;
94
+ }
95
+
96
+ export interface CompressionOptions {
97
+ threshold?: number;
98
+ level?: number;
99
+ algorithms?: ("gzip" | "deflate" | "br")[];
100
+ filter?: (req: AzuraRequest, res: AzuraResponse) => boolean;
101
+ }
102
+
103
+ export interface StaticOptions {
104
+ root: string;
105
+ prefix?: string;
106
+ index?: string | string[];
107
+ dotfiles?: "allow" | "deny" | "ignore";
108
+ maxAge?: number;
109
+ etag?: boolean;
110
+ lastModified?: boolean;
111
+ fallthrough?: boolean;
112
+ immutable?: boolean;
113
+ }
114
+
115
+ export interface ETagOptions {
116
+ weak?: boolean;
117
+ }
118
+
119
+ export interface RequestIdOptions {
120
+ header?: string;
121
+ generator?: () => string;
122
+ }
123
+
124
+ export interface TimeoutOptions {
125
+ timeout: number;
126
+ message?: string | object;
127
+ statusCode?: number;
128
+ }
129
+
130
+ export interface HealthCheckOptions {
131
+ path?: string;
132
+ checks?: Record<string, () => Promise<boolean> | boolean>;
133
+ timeout?: number;
134
+ }
135
+
136
+ export interface CircuitBreakerOptions {
137
+ threshold?: number;
138
+ timeout?: number;
139
+ resetTimeout?: number;
140
+ halfOpenRequests?: number;
141
+ monitor?: (state: CircuitBreakerState) => void;
142
+ }
143
+
144
+ export type CircuitBreakerState = "CLOSED" | "OPEN" | "HALF_OPEN";
145
+
146
+ export interface ProxyOptions {
147
+ target: string;
148
+ pathRewrite?: Record<string, string>;
149
+ changeOrigin?: boolean;
150
+ timeout?: number;
151
+ headers?: Record<string, string>;
152
+ onProxyReq?: (proxyReq: any, req: AzuraRequest) => void;
153
+ onProxyRes?: (proxyRes: any, res: AzuraResponse) => void;
154
+ }
155
+
156
+ export interface SSEOptions {
157
+ path: string;
158
+ heartbeatInterval?: number;
159
+ retry?: number;
160
+ maxClients?: number;
161
+ }
162
+
163
+ export interface MultipartOptions {
164
+ maxFileSize?: number;
165
+ maxFiles?: number;
166
+ maxFieldSize?: number;
167
+ maxFields?: number;
168
+ allowedMimeTypes?: string[];
169
+ uploadDir?: string;
170
+ }
@@ -0,0 +1,14 @@
1
+ declare namespace Reflect {
2
+ function defineMetadata(
3
+ metadataKey: string,
4
+ metadataValue: any,
5
+ target: any,
6
+ propertyKey?: string,
7
+ ): void;
8
+
9
+ function getMetadata(
10
+ metadataKey: string,
11
+ target: any,
12
+ propertyKey?: string,
13
+ ): any;
14
+ }
@@ -0,0 +1,70 @@
1
+ import type { HttpMethod, MiddlewareHandler, RouteHandler } from "./common.type.js";
2
+
3
+ export interface RouteDefinition {
4
+ method: HttpMethod;
5
+ path: string;
6
+ handler: RouteHandler;
7
+ middlewares?: MiddlewareHandler[];
8
+ meta?: RouteMeta;
9
+ }
10
+
11
+ export interface RouteMeta {
12
+ summary?: string;
13
+ description?: string;
14
+ tags?: string[];
15
+ deprecated?: boolean;
16
+ produces?: string[];
17
+ consumes?: string[];
18
+ parameters?: ParameterMeta[];
19
+ responses?: Record<number, ResponseMeta>;
20
+ security?: Record<string, string[]>[];
21
+ }
22
+
23
+ export interface ParameterMeta {
24
+ name: string;
25
+ in: "path" | "query" | "header" | "body";
26
+ required?: boolean;
27
+ type?: string;
28
+ description?: string;
29
+ schema?: any;
30
+ }
31
+
32
+ export interface ResponseMeta {
33
+ description: string;
34
+ schema?: any;
35
+ headers?: Record<string, { type: string; description: string }>;
36
+ }
37
+
38
+ export type ParamType =
39
+ | "body"
40
+ | "query"
41
+ | "param"
42
+ | "header"
43
+ | "cookie"
44
+ | "req"
45
+ | "res"
46
+ | "next"
47
+ | "ip"
48
+ | "session";
49
+
50
+ export interface ParamMetadata {
51
+ index: number;
52
+ type: ParamType;
53
+ name?: string;
54
+ pipes?: ((value: any) => any)[];
55
+ }
56
+
57
+ export interface ControllerMetadata {
58
+ prefix: string;
59
+ middlewares: MiddlewareHandler[];
60
+ routes: Map<string, RouteMetadata>;
61
+ }
62
+
63
+ export interface RouteMetadata {
64
+ method: HttpMethod;
65
+ path: string;
66
+ propertyKey: string;
67
+ middlewares: MiddlewareHandler[];
68
+ params: ParamMetadata[];
69
+ meta?: RouteMeta;
70
+ }
@@ -0,0 +1,62 @@
1
+ import { HttpStatusText } from "../types/http/status.js";
2
+
3
+ export class HttpError extends Error {
4
+ public readonly statusCode: number;
5
+ public readonly details?: any;
6
+ public readonly isOperational: boolean;
7
+
8
+ constructor(statusCode: number, message?: string, details?: any) {
9
+ super(message ?? HttpStatusText[statusCode] ?? "Unknown Error");
10
+ this.statusCode = statusCode;
11
+ this.details = details;
12
+ this.isOperational = true;
13
+ Object.setPrototypeOf(this, HttpError.prototype);
14
+ }
15
+
16
+ toJSON(): Record<string, any> {
17
+ const obj: Record<string, any> = {
18
+ error: {
19
+ statusCode: this.statusCode,
20
+ message: this.message,
21
+ },
22
+ };
23
+ if (this.details) obj.error.details = this.details;
24
+ return obj;
25
+ }
26
+
27
+ static badRequest(message?: string, details?: any): HttpError {
28
+ return new HttpError(400, message ?? "Bad Request", details);
29
+ }
30
+
31
+ static unauthorized(message?: string): HttpError {
32
+ return new HttpError(401, message ?? "Unauthorized");
33
+ }
34
+
35
+ static forbidden(message?: string): HttpError {
36
+ return new HttpError(403, message ?? "Forbidden");
37
+ }
38
+
39
+ static notFound(message?: string): HttpError {
40
+ return new HttpError(404, message ?? "Not Found");
41
+ }
42
+
43
+ static methodNotAllowed(message?: string): HttpError {
44
+ return new HttpError(405, message ?? "Method Not Allowed");
45
+ }
46
+
47
+ static conflict(message?: string, details?: any): HttpError {
48
+ return new HttpError(409, message ?? "Conflict", details);
49
+ }
50
+
51
+ static unprocessableEntity(message?: string, details?: any): HttpError {
52
+ return new HttpError(422, message ?? "Unprocessable Entity", details);
53
+ }
54
+
55
+ static tooManyRequests(message?: string): HttpError {
56
+ return new HttpError(429, message ?? "Too Many Requests");
57
+ }
58
+
59
+ static internal(message?: string): HttpError {
60
+ return new HttpError(500, message ?? "Internal Server Error");
61
+ }
62
+ }
@@ -0,0 +1,30 @@
1
+ import type { IncomingMessage } from "node:http";
2
+
3
+ const PROXY_HEADERS = [
4
+ "x-forwarded-for",
5
+ "x-real-ip",
6
+ "cf-connecting-ip",
7
+ "x-client-ip",
8
+ "x-cluster-client-ip",
9
+ "fastly-client-ip",
10
+ "true-client-ip",
11
+ ] as const;
12
+
13
+ export function resolveIp(req: IncomingMessage): string {
14
+ for (const header of PROXY_HEADERS) {
15
+ const val = req.headers[header];
16
+ if (val) {
17
+ const ip = typeof val === "string" ? val.split(",")[0].trim() : val[0];
18
+ if (ip && ip.length > 0) return normalizeIp(ip);
19
+ }
20
+ }
21
+
22
+ const remoteAddr = req.socket?.remoteAddress ?? "127.0.0.1";
23
+ return normalizeIp(remoteAddr);
24
+ }
25
+
26
+ function normalizeIp(ip: string): string {
27
+ if (ip === "::1" || ip === "::ffff:127.0.0.1") return "127.0.0.1";
28
+ if (ip.startsWith("::ffff:")) return ip.slice(7);
29
+ return ip;
30
+ }
@@ -0,0 +1,144 @@
1
+ type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
2
+
3
+ const LEVEL_PRIORITY: Record<LogLevel, number> = {
4
+ debug: 0,
5
+ info: 1,
6
+ warn: 2,
7
+ error: 3,
8
+ silent: 4,
9
+ };
10
+
11
+ const COLORS = {
12
+ reset: "\x1b[0m",
13
+ bold: "\x1b[1m",
14
+ dim: "\x1b[2m",
15
+ red: "\x1b[31m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ blue: "\x1b[34m",
19
+ magenta: "\x1b[35m",
20
+ cyan: "\x1b[36m",
21
+ white: "\x1b[37m",
22
+ gray: "\x1b[90m",
23
+ bgRed: "\x1b[41m",
24
+ bgGreen: "\x1b[42m",
25
+ bgYellow: "\x1b[43m",
26
+ bgBlue: "\x1b[44m",
27
+ } as const;
28
+
29
+ const METHOD_COLORS: Record<string, string> = {
30
+ GET: COLORS.green,
31
+ POST: COLORS.blue,
32
+ PUT: COLORS.yellow,
33
+ DELETE: COLORS.red,
34
+ PATCH: COLORS.magenta,
35
+ HEAD: COLORS.cyan,
36
+ OPTIONS: COLORS.gray,
37
+ };
38
+
39
+ function statusColor(code: number): string {
40
+ if (code < 200) return COLORS.gray;
41
+ if (code < 300) return COLORS.green;
42
+ if (code < 400) return COLORS.cyan;
43
+ if (code < 500) return COLORS.yellow;
44
+ return COLORS.red;
45
+ }
46
+
47
+ function formatDuration(ns: bigint): string {
48
+ const us = Number(ns) / 1_000;
49
+ if (us < 1_000) return `${us.toFixed(0)}µs`;
50
+ const ms = us / 1_000;
51
+ if (ms < 1_000) return `${ms.toFixed(1)}ms`;
52
+ return `${(ms / 1_000).toFixed(2)}s`;
53
+ }
54
+
55
+ export class Logger {
56
+ private level: LogLevel;
57
+ private useColors: boolean;
58
+ private showTimestamp: boolean;
59
+ private prefix: string;
60
+
61
+ constructor(options: {
62
+ level?: LogLevel;
63
+ colors?: boolean;
64
+ timestamp?: boolean;
65
+ prefix?: string;
66
+ } = {}) {
67
+ this.level = options.level ?? "info";
68
+ this.useColors = options.colors ?? process.stdout.isTTY !== false;
69
+ this.showTimestamp = options.timestamp ?? true;
70
+ this.prefix = options.prefix ?? "azura";
71
+ }
72
+
73
+ private shouldLog(level: LogLevel): boolean {
74
+ return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];
75
+ }
76
+
77
+ private timestamp(): string {
78
+ if (!this.showTimestamp) return "";
79
+ const now = new Date();
80
+ return `${COLORS.gray}${now.toISOString().slice(11, 23)}${COLORS.reset} `;
81
+ }
82
+
83
+ private tag(level: LogLevel): string {
84
+ if (!this.useColors) return `[${level.toUpperCase()}]`;
85
+ const colorMap: Record<string, string> = {
86
+ debug: COLORS.gray,
87
+ info: COLORS.blue,
88
+ warn: COLORS.yellow,
89
+ error: COLORS.red,
90
+ };
91
+ return `${colorMap[level] ?? ""}[${level.toUpperCase()}]${COLORS.reset}`;
92
+ }
93
+
94
+ debug(message: string, ...args: any[]): void {
95
+ if (!this.shouldLog("debug")) return;
96
+ console.debug(`${this.timestamp()}${this.tag("debug")} ${message}`, ...args);
97
+ }
98
+
99
+ info(message: string, ...args: any[]): void {
100
+ if (!this.shouldLog("info")) return;
101
+ console.info(`${this.timestamp()}${this.tag("info")} ${message}`, ...args);
102
+ }
103
+
104
+ warn(message: string, ...args: any[]): void {
105
+ if (!this.shouldLog("warn")) return;
106
+ console.warn(`${this.timestamp()}${this.tag("warn")} ${message}`, ...args);
107
+ }
108
+
109
+ error(message: string, ...args: any[]): void {
110
+ if (!this.shouldLog("error")) return;
111
+ console.error(`${this.timestamp()}${this.tag("error")} ${message}`, ...args);
112
+ }
113
+
114
+ request(method: string, path: string, statusCode: number, duration: bigint): void {
115
+ if (!this.shouldLog("info")) return;
116
+ const mc = this.useColors ? METHOD_COLORS[method] ?? COLORS.white : "";
117
+ const sc = this.useColors ? statusColor(statusCode) : "";
118
+ const r = this.useColors ? COLORS.reset : "";
119
+ const dur = formatDuration(duration);
120
+ console.info(
121
+ `${this.timestamp()}${mc}${method.padEnd(7)}${r} ${path} ${sc}${statusCode}${r} ${COLORS.dim}${dur}${r}`,
122
+ );
123
+ }
124
+
125
+ banner(port: number, host: string): void {
126
+ if (!this.shouldLog("info")) return;
127
+ const c = this.useColors;
128
+ const lines = [
129
+ "",
130
+ `${c ? COLORS.bold + COLORS.cyan : ""} ⚡ AzuraJS v3.0.0${c ? COLORS.reset : ""}`,
131
+ "",
132
+ `${c ? COLORS.green : ""} ➜ Local: ${c ? COLORS.bold : ""}http://${host}:${port}/${c ? COLORS.reset : ""}`,
133
+ `${c ? COLORS.dim : ""} ➜ Press Ctrl+C to stop${c ? COLORS.reset : ""}`,
134
+ "",
135
+ ];
136
+ console.info(lines.join("\n"));
137
+ }
138
+
139
+ setLevel(level: LogLevel): void {
140
+ this.level = level;
141
+ }
142
+ }
143
+
144
+ export const logger = new Logger();
@@ -0,0 +1,182 @@
1
+ import type { IncomingMessage } from "node:http";
2
+
3
+ export function parseQueryString(qs: string): Record<string, string | string[]> {
4
+ const result: Record<string, string | string[]> = Object.create(null);
5
+ if (!qs || qs.length === 0) return result;
6
+
7
+ let key = "";
8
+ let value = "";
9
+ let startingKey = true;
10
+ let i = qs.charCodeAt(0) === 63 ? 1 : 0; // skip leading ?
11
+
12
+ for (; i < qs.length; i++) {
13
+ const ch = qs.charCodeAt(i);
14
+
15
+ if (ch === 61 /* = */ && startingKey) {
16
+ startingKey = false;
17
+ continue;
18
+ }
19
+
20
+ if (ch === 38 /* & */) {
21
+ if (key.length > 0) {
22
+ const decodedKey = fastDecode(key);
23
+ const decodedVal = fastDecode(value);
24
+ const existing = result[decodedKey];
25
+ if (existing === undefined) {
26
+ result[decodedKey] = decodedVal;
27
+ } else if (typeof existing === "string") {
28
+ result[decodedKey] = [existing, decodedVal];
29
+ } else {
30
+ existing.push(decodedVal);
31
+ }
32
+ }
33
+ key = "";
34
+ value = "";
35
+ startingKey = true;
36
+ continue;
37
+ }
38
+
39
+ if (startingKey) {
40
+ key += qs[i];
41
+ } else {
42
+ value += qs[i];
43
+ }
44
+ }
45
+
46
+ if (key.length > 0) {
47
+ const decodedKey = fastDecode(key);
48
+ const decodedVal = fastDecode(value);
49
+ const existing = result[decodedKey];
50
+ if (existing === undefined) {
51
+ result[decodedKey] = decodedVal;
52
+ } else if (typeof existing === "string") {
53
+ result[decodedKey] = [existing, decodedVal];
54
+ } else {
55
+ existing.push(decodedVal);
56
+ }
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ function fastDecode(str: string): string {
63
+ if (str.indexOf("%") === -1 && str.indexOf("+") === -1) return str;
64
+ try {
65
+ return decodeURIComponent(str.replace(/\+/g, " "));
66
+ } catch {
67
+ return str;
68
+ }
69
+ }
70
+
71
+ export function parseCookies(header: string | undefined): Record<string, string> {
72
+ const cookies: Record<string, string> = Object.create(null);
73
+ if (!header) return cookies;
74
+
75
+ let i = 0;
76
+ const len = header.length;
77
+
78
+ while (i < len) {
79
+ while (i < len && header.charCodeAt(i) === 32) i++; // skip spaces
80
+
81
+ let eqIdx = -1;
82
+ let semiIdx = -1;
83
+ for (let j = i; j < len; j++) {
84
+ const ch = header.charCodeAt(j);
85
+ if (ch === 61 && eqIdx === -1) eqIdx = j;
86
+ if (ch === 59) { semiIdx = j; break; }
87
+ }
88
+
89
+ if (eqIdx === -1) {
90
+ i = semiIdx === -1 ? len : semiIdx + 1;
91
+ continue;
92
+ }
93
+
94
+ const end = semiIdx === -1 ? len : semiIdx;
95
+ const name = header.slice(i, eqIdx).trim();
96
+ let val = header.slice(eqIdx + 1, end).trim();
97
+
98
+ if (val.charCodeAt(0) === 34 && val.charCodeAt(val.length - 1) === 34) {
99
+ val = val.slice(1, -1);
100
+ }
101
+
102
+ if (cookies[name] === undefined) {
103
+ cookies[name] = fastDecode(val);
104
+ }
105
+
106
+ i = end + 1;
107
+ }
108
+
109
+ return cookies;
110
+ }
111
+
112
+ const CONTENT_TYPE_JSON = "application/json";
113
+ const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
114
+
115
+ export async function parseBody(req: IncomingMessage): Promise<any> {
116
+ const contentType = req.headers["content-type"] ?? "";
117
+ const contentLength = req.headers["content-length"];
118
+
119
+ if (
120
+ req.method === "GET" ||
121
+ req.method === "HEAD" ||
122
+ req.method === "OPTIONS" ||
123
+ (contentLength !== undefined && contentLength === "0")
124
+ ) {
125
+ return undefined;
126
+ }
127
+
128
+ const chunks: Buffer[] = [];
129
+ let totalSize = 0;
130
+ const maxSize = 10 * 1024 * 1024; // 10MB default
131
+
132
+ return new Promise((resolve, reject) => {
133
+ req.on("data", (chunk: Buffer) => {
134
+ totalSize += chunk.length;
135
+ if (totalSize > maxSize) {
136
+ req.destroy();
137
+ reject(new Error("Request body too large"));
138
+ return;
139
+ }
140
+ chunks.push(chunk);
141
+ });
142
+
143
+ req.on("end", () => {
144
+ if (chunks.length === 0) {
145
+ resolve(undefined);
146
+ return;
147
+ }
148
+
149
+ const raw = Buffer.concat(chunks).toString("utf-8");
150
+
151
+ if (contentType.startsWith(CONTENT_TYPE_JSON)) {
152
+ try {
153
+ resolve(JSON.parse(raw));
154
+ } catch {
155
+ resolve(raw);
156
+ }
157
+ } else if (contentType.startsWith(CONTENT_TYPE_FORM)) {
158
+ resolve(parseQueryString(raw));
159
+ } else {
160
+ resolve(raw);
161
+ }
162
+ });
163
+
164
+ req.on("error", reject);
165
+ });
166
+ }
167
+
168
+ export function parseUrl(url: string): { pathname: string; search: string } {
169
+ let pathname = "";
170
+ let search = "";
171
+ let i = 0;
172
+
173
+ for (; i < url.length; i++) {
174
+ if (url.charCodeAt(i) === 63) { // ?
175
+ pathname = url.slice(0, i);
176
+ search = url.slice(i);
177
+ return { pathname, search };
178
+ }
179
+ }
180
+
181
+ return { pathname: url, search: "" };
182
+ }