@forinda/kickjs-http 0.3.1 → 0.4.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/dist/application.js +4 -4
- package/dist/bootstrap.js +5 -5
- package/dist/{chunk-ZI52TGQ4.js → chunk-35NUARK7.js} +2 -2
- package/dist/{chunk-BNWCVQQH.js → chunk-3NEDJA3J.js} +2 -2
- package/dist/{chunk-P3YCN5LK.js → chunk-4G2S7T4R.js} +6 -6
- package/dist/chunk-DUQ7SN7N.js +208 -0
- package/dist/chunk-DUQ7SN7N.js.map +1 -0
- package/dist/chunk-H4S527PH.js +97 -0
- package/dist/chunk-H4S527PH.js.map +1 -0
- package/dist/{chunk-I6UNTOQD.js → chunk-I32MVBEG.js} +2 -2
- package/dist/chunk-LEILPDMW.js +183 -0
- package/dist/chunk-LEILPDMW.js.map +1 -0
- package/dist/{chunk-RZUH6NBM.js → chunk-LQ6RSWMX.js} +7 -3
- package/dist/chunk-LQ6RSWMX.js.map +1 -0
- package/dist/chunk-NQJNMKW5.js +158 -0
- package/dist/chunk-NQJNMKW5.js.map +1 -0
- package/dist/{chunk-KAWXFLFS.js → chunk-OWLI3SBW.js} +14 -4
- package/dist/chunk-OWLI3SBW.js.map +1 -0
- package/dist/{chunk-JD2RKDKH.js → chunk-RPN7UFUO.js} +2 -2
- package/dist/{chunk-U2JYL2NW.js → chunk-VFVMIFNZ.js} +11 -4
- package/dist/chunk-VFVMIFNZ.js.map +1 -0
- package/dist/{chunk-JM7X7SAD.js → chunk-VXX2Y3TA.js} +2 -2
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.js +3 -3
- package/dist/devtools.d.ts +85 -0
- package/dist/devtools.js +8 -0
- package/dist/devtools.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +32 -16
- package/dist/middleware/csrf.js +2 -2
- package/dist/middleware/error-handler.js +2 -2
- package/dist/middleware/rate-limit.d.ts +53 -0
- package/dist/middleware/rate-limit.js +8 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-id.js +2 -2
- package/dist/middleware/session.d.ts +64 -0
- package/dist/middleware/session.js +8 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/upload.d.ts +35 -16
- package/dist/middleware/upload.js +6 -2
- package/dist/middleware/validate.js +2 -2
- package/dist/query/index.js +2 -2
- package/dist/router-builder.js +6 -5
- package/package.json +15 -2
- package/dist/chunk-75Z5FSZN.js +0 -88
- package/dist/chunk-75Z5FSZN.js.map +0 -1
- package/dist/chunk-7QVYU63E.js +0 -7
- package/dist/chunk-KAWXFLFS.js.map +0 -1
- package/dist/chunk-RZUH6NBM.js.map +0 -1
- package/dist/chunk-U2JYL2NW.js.map +0 -1
- /package/dist/{chunk-ZI52TGQ4.js.map → chunk-35NUARK7.js.map} +0 -0
- /package/dist/{chunk-BNWCVQQH.js.map → chunk-3NEDJA3J.js.map} +0 -0
- /package/dist/{chunk-P3YCN5LK.js.map → chunk-4G2S7T4R.js.map} +0 -0
- /package/dist/{chunk-I6UNTOQD.js.map → chunk-I32MVBEG.js.map} +0 -0
- /package/dist/{chunk-JD2RKDKH.js.map → chunk-RPN7UFUO.js.map} +0 -0
- /package/dist/{chunk-JM7X7SAD.js.map → chunk-VXX2Y3TA.js.map} +0 -0
- /package/dist/{chunk-7QVYU63E.js.map → chunk-WCQVDF3K.js.map} +0 -0
package/dist/application.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Application
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-4G2S7T4R.js";
|
|
4
|
+
import "./chunk-35NUARK7.js";
|
|
5
|
+
import "./chunk-3NEDJA3J.js";
|
|
6
|
+
import "./chunk-WCQVDF3K.js";
|
|
7
7
|
export {
|
|
8
8
|
Application
|
|
9
9
|
};
|
package/dist/bootstrap.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bootstrap
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-OWLI3SBW.js";
|
|
4
|
+
import "./chunk-4G2S7T4R.js";
|
|
5
|
+
import "./chunk-35NUARK7.js";
|
|
6
|
+
import "./chunk-3NEDJA3J.js";
|
|
7
|
+
import "./chunk-WCQVDF3K.js";
|
|
8
8
|
export {
|
|
9
9
|
bootstrap
|
|
10
10
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__name
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/request-id.ts
|
|
6
6
|
import { randomUUID } from "crypto";
|
|
@@ -19,4 +19,4 @@ export {
|
|
|
19
19
|
REQUEST_ID_HEADER,
|
|
20
20
|
requestId
|
|
21
21
|
};
|
|
22
|
-
//# sourceMappingURL=chunk-
|
|
22
|
+
//# sourceMappingURL=chunk-35NUARK7.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__name
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/error-handler.ts
|
|
6
6
|
import { HttpException, createLogger } from "@forinda/kickjs-core";
|
|
@@ -51,4 +51,4 @@ export {
|
|
|
51
51
|
notFoundHandler,
|
|
52
52
|
errorHandler
|
|
53
53
|
};
|
|
54
|
-
//# sourceMappingURL=chunk-
|
|
54
|
+
//# sourceMappingURL=chunk-3NEDJA3J.js.map
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requestId
|
|
3
|
+
} from "./chunk-35NUARK7.js";
|
|
1
4
|
import {
|
|
2
5
|
errorHandler,
|
|
3
6
|
notFoundHandler
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
requestId
|
|
7
|
-
} from "./chunk-ZI52TGQ4.js";
|
|
7
|
+
} from "./chunk-3NEDJA3J.js";
|
|
8
8
|
import {
|
|
9
9
|
__name
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WCQVDF3K.js";
|
|
11
11
|
|
|
12
12
|
// src/application.ts
|
|
13
13
|
import http from "http";
|
|
@@ -193,4 +193,4 @@ var Application = class {
|
|
|
193
193
|
export {
|
|
194
194
|
Application
|
|
195
195
|
};
|
|
196
|
-
//# sourceMappingURL=chunk-
|
|
196
|
+
//# sourceMappingURL=chunk-4G2S7T4R.js.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
|
+
|
|
5
|
+
// src/devtools.ts
|
|
6
|
+
import { Router } from "express";
|
|
7
|
+
import { METADATA, ref, computed, reactive, watch, createLogger } from "@forinda/kickjs-core";
|
|
8
|
+
var log = createLogger("DevTools");
|
|
9
|
+
var DevToolsAdapter = class {
|
|
10
|
+
static {
|
|
11
|
+
__name(this, "DevToolsAdapter");
|
|
12
|
+
}
|
|
13
|
+
name = "DevToolsAdapter";
|
|
14
|
+
basePath;
|
|
15
|
+
enabled;
|
|
16
|
+
exposeConfig;
|
|
17
|
+
configPrefixes;
|
|
18
|
+
errorRateThreshold;
|
|
19
|
+
// ── Reactive State ───────────────────────────────────────────────────
|
|
20
|
+
/** Total requests received */
|
|
21
|
+
requestCount;
|
|
22
|
+
/** Total responses with status >= 500 */
|
|
23
|
+
errorCount;
|
|
24
|
+
/** Total responses with status >= 400 and < 500 */
|
|
25
|
+
clientErrorCount;
|
|
26
|
+
/** Server start time */
|
|
27
|
+
startedAt;
|
|
28
|
+
/** Computed error rate (server errors / total requests) */
|
|
29
|
+
errorRate;
|
|
30
|
+
/** Computed uptime in seconds */
|
|
31
|
+
uptimeSeconds;
|
|
32
|
+
/** Per-route latency tracking */
|
|
33
|
+
routeLatency;
|
|
34
|
+
// ── Internal State ───────────────────────────────────────────────────
|
|
35
|
+
routes = [];
|
|
36
|
+
container = null;
|
|
37
|
+
adapterStatuses = {};
|
|
38
|
+
stopErrorWatch = null;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.basePath = options.basePath ?? "/_debug";
|
|
41
|
+
this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
|
|
42
|
+
this.exposeConfig = options.exposeConfig ?? false;
|
|
43
|
+
this.configPrefixes = options.configPrefixes ?? [
|
|
44
|
+
"APP_",
|
|
45
|
+
"NODE_ENV"
|
|
46
|
+
];
|
|
47
|
+
this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
|
|
48
|
+
this.requestCount = ref(0);
|
|
49
|
+
this.errorCount = ref(0);
|
|
50
|
+
this.clientErrorCount = ref(0);
|
|
51
|
+
this.startedAt = ref(Date.now());
|
|
52
|
+
this.routeLatency = reactive({});
|
|
53
|
+
this.errorRate = computed(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0);
|
|
54
|
+
this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1e3));
|
|
55
|
+
if (options.onErrorRateExceeded) {
|
|
56
|
+
const callback = options.onErrorRateExceeded;
|
|
57
|
+
const threshold = this.errorRateThreshold;
|
|
58
|
+
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
59
|
+
if (rate > threshold) {
|
|
60
|
+
callback(rate);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
65
|
+
if (rate > this.errorRateThreshold) {
|
|
66
|
+
log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ── Adapter Lifecycle ────────────────────────────────────────────────
|
|
72
|
+
beforeMount(app, container) {
|
|
73
|
+
if (!this.enabled) return;
|
|
74
|
+
this.container = container;
|
|
75
|
+
this.startedAt.value = Date.now();
|
|
76
|
+
const router = Router();
|
|
77
|
+
router.get("/routes", (_req, res) => {
|
|
78
|
+
res.json({
|
|
79
|
+
routes: this.routes
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
router.get("/container", (_req, res) => {
|
|
83
|
+
const registrations = this.container?.getRegistrations() ?? [];
|
|
84
|
+
res.json({
|
|
85
|
+
registrations,
|
|
86
|
+
count: registrations.length
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
router.get("/metrics", (_req, res) => {
|
|
90
|
+
res.json({
|
|
91
|
+
requests: this.requestCount.value,
|
|
92
|
+
serverErrors: this.errorCount.value,
|
|
93
|
+
clientErrors: this.clientErrorCount.value,
|
|
94
|
+
errorRate: this.errorRate.value,
|
|
95
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
96
|
+
startedAt: new Date(this.startedAt.value).toISOString(),
|
|
97
|
+
routeLatency: this.routeLatency
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
router.get("/health", (_req, res) => {
|
|
101
|
+
const healthy = this.errorRate.value < this.errorRateThreshold;
|
|
102
|
+
const status = healthy ? "healthy" : "degraded";
|
|
103
|
+
res.status(healthy ? 200 : 503).json({
|
|
104
|
+
status,
|
|
105
|
+
errorRate: this.errorRate.value,
|
|
106
|
+
uptime: this.uptimeSeconds.value,
|
|
107
|
+
adapters: this.adapterStatuses
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
router.get("/state", (_req, res) => {
|
|
111
|
+
res.json({
|
|
112
|
+
reactive: {
|
|
113
|
+
requestCount: this.requestCount.value,
|
|
114
|
+
errorCount: this.errorCount.value,
|
|
115
|
+
clientErrorCount: this.clientErrorCount.value,
|
|
116
|
+
errorRate: this.errorRate.value,
|
|
117
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
118
|
+
startedAt: new Date(this.startedAt.value).toISOString()
|
|
119
|
+
},
|
|
120
|
+
routes: this.routes.length,
|
|
121
|
+
container: this.container?.getRegistrations().length ?? 0,
|
|
122
|
+
routeLatency: this.routeLatency
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
if (this.exposeConfig) {
|
|
126
|
+
router.get("/config", (_req, res) => {
|
|
127
|
+
const config = {};
|
|
128
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
129
|
+
if (value === void 0) continue;
|
|
130
|
+
const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix));
|
|
131
|
+
config[key] = allowed ? value : "[REDACTED]";
|
|
132
|
+
}
|
|
133
|
+
res.json({
|
|
134
|
+
config
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
app.use(this.basePath, router);
|
|
139
|
+
log.info(`DevTools mounted at ${this.basePath}`);
|
|
140
|
+
}
|
|
141
|
+
middleware() {
|
|
142
|
+
if (!this.enabled) return [];
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
handler: /* @__PURE__ */ __name((req, res, next) => {
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
this.requestCount.value++;
|
|
148
|
+
res.on("finish", () => {
|
|
149
|
+
if (res.statusCode >= 500) this.errorCount.value++;
|
|
150
|
+
else if (res.statusCode >= 400) this.clientErrorCount.value++;
|
|
151
|
+
const routeKey = `${req.method} ${req.route?.path ?? req.path}`;
|
|
152
|
+
const elapsed = Date.now() - start;
|
|
153
|
+
if (!this.routeLatency[routeKey]) {
|
|
154
|
+
this.routeLatency[routeKey] = {
|
|
155
|
+
count: 0,
|
|
156
|
+
totalMs: 0,
|
|
157
|
+
minMs: Infinity,
|
|
158
|
+
maxMs: 0
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const stats = this.routeLatency[routeKey];
|
|
162
|
+
stats.count++;
|
|
163
|
+
stats.totalMs += elapsed;
|
|
164
|
+
stats.minMs = Math.min(stats.minMs, elapsed);
|
|
165
|
+
stats.maxMs = Math.max(stats.maxMs, elapsed);
|
|
166
|
+
});
|
|
167
|
+
next();
|
|
168
|
+
}, "handler"),
|
|
169
|
+
phase: "beforeGlobal"
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
onRouteMount(controllerClass, mountPath) {
|
|
174
|
+
if (!this.enabled) return;
|
|
175
|
+
const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
|
|
176
|
+
const classMiddleware = Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? [];
|
|
177
|
+
for (const route of routes) {
|
|
178
|
+
const methodMiddleware = Reflect.getMetadata(METADATA.METHOD_MIDDLEWARES, controllerClass.prototype, route.handlerName) ?? [];
|
|
179
|
+
this.routes.push({
|
|
180
|
+
method: route.method.toUpperCase(),
|
|
181
|
+
path: `${mountPath}${route.path === "/" ? "" : route.path}`,
|
|
182
|
+
controller: controllerClass.name,
|
|
183
|
+
handler: route.handlerName,
|
|
184
|
+
middleware: [
|
|
185
|
+
...classMiddleware.map((m) => m.name || "anonymous"),
|
|
186
|
+
...methodMiddleware.map((m) => m.name || "anonymous")
|
|
187
|
+
]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
beforeStart(_app, _container) {
|
|
192
|
+
if (!this.enabled) return;
|
|
193
|
+
this.adapterStatuses[this.name] = "running";
|
|
194
|
+
}
|
|
195
|
+
afterStart(_server, _container) {
|
|
196
|
+
if (!this.enabled) return;
|
|
197
|
+
log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
|
|
198
|
+
}
|
|
199
|
+
shutdown() {
|
|
200
|
+
this.stopErrorWatch?.();
|
|
201
|
+
this.adapterStatuses[this.name] = "stopped";
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export {
|
|
206
|
+
DevToolsAdapter
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=chunk-DUQ7SN7N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devtools.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router } from 'express'\nimport {\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\n this.container = container\n this.startedAt.value = Date.now()\n\n const router = Router()\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: this.routeLatency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n app.use(this.basePath, router)\n log.info(`DevTools mounted at ${this.basePath}`)\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n ) ?? []\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n beforeStart(_app: any, _container: Container): void {\n if (!this.enabled) return\n this.adapterStatuses[this.name] = 'running'\n }\n\n afterStart(_server: any, _container: Container): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n}\n"],"mappings":"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","names":["Router","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","options","process","env","NODE_ENV","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","get","_req","res","json","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","status","uptime","adapters","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","use","info","middleware","handler","req","next","start","on","statusCode","routeKey","method","route","path","elapsed","totalMs","minMs","Infinity","maxMs","stats","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","beforeStart","_app","_container","afterStart","_server","shutdown"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware/rate-limit.ts
|
|
6
|
+
var MemoryStore = class MemoryStore2 {
|
|
7
|
+
static {
|
|
8
|
+
__name(this, "MemoryStore");
|
|
9
|
+
}
|
|
10
|
+
windowMs;
|
|
11
|
+
hits = /* @__PURE__ */ new Map();
|
|
12
|
+
cleanupTimer;
|
|
13
|
+
constructor(windowMs) {
|
|
14
|
+
this.windowMs = windowMs;
|
|
15
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), windowMs);
|
|
16
|
+
if (this.cleanupTimer.unref) {
|
|
17
|
+
this.cleanupTimer.unref();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async increment(key) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const entry = this.hits.get(key);
|
|
23
|
+
if (entry && entry.resetTime.getTime() > now) {
|
|
24
|
+
entry.totalHits++;
|
|
25
|
+
return {
|
|
26
|
+
totalHits: entry.totalHits,
|
|
27
|
+
resetTime: entry.resetTime
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const resetTime = new Date(now + this.windowMs);
|
|
31
|
+
const newEntry = {
|
|
32
|
+
totalHits: 1,
|
|
33
|
+
resetTime
|
|
34
|
+
};
|
|
35
|
+
this.hits.set(key, newEntry);
|
|
36
|
+
return {
|
|
37
|
+
totalHits: 1,
|
|
38
|
+
resetTime
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async decrement(key) {
|
|
42
|
+
const entry = this.hits.get(key);
|
|
43
|
+
if (entry && entry.totalHits > 0) {
|
|
44
|
+
entry.totalHits--;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async reset(key) {
|
|
48
|
+
this.hits.delete(key);
|
|
49
|
+
}
|
|
50
|
+
cleanup() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
for (const [key, entry] of this.hits) {
|
|
53
|
+
if (entry.resetTime.getTime() <= now) {
|
|
54
|
+
this.hits.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function rateLimit(options = {}) {
|
|
60
|
+
const max = options.max ?? 100;
|
|
61
|
+
const windowMs = options.windowMs ?? 6e4;
|
|
62
|
+
const message = options.message ?? "Too Many Requests";
|
|
63
|
+
const statusCode = options.statusCode ?? 429;
|
|
64
|
+
const keyGenerator = options.keyGenerator ?? ((req) => req.ip ?? "127.0.0.1");
|
|
65
|
+
const sendHeaders = options.headers ?? true;
|
|
66
|
+
const store = options.store ?? new MemoryStore(windowMs);
|
|
67
|
+
const skip = options.skip;
|
|
68
|
+
const skipPaths = new Set(options.skipPaths ?? []);
|
|
69
|
+
return async (req, res, next) => {
|
|
70
|
+
if (skipPaths.has(req.path)) {
|
|
71
|
+
return next();
|
|
72
|
+
}
|
|
73
|
+
if (skip && skip(req)) {
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
const key = keyGenerator(req);
|
|
77
|
+
const { totalHits, resetTime } = await store.increment(key);
|
|
78
|
+
const remaining = Math.max(0, max - totalHits);
|
|
79
|
+
if (sendHeaders) {
|
|
80
|
+
res.setHeader("RateLimit-Limit", max);
|
|
81
|
+
res.setHeader("RateLimit-Remaining", remaining);
|
|
82
|
+
res.setHeader("RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
|
83
|
+
}
|
|
84
|
+
if (totalHits > max) {
|
|
85
|
+
return res.status(statusCode).json({
|
|
86
|
+
message
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
next();
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
__name(rateLimit, "rateLimit");
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
rateLimit
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=chunk-H4S527PH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\n\nexport interface RateLimitStore {\n increment(key: string): Promise<{ totalHits: number; resetTime: Date }>\n decrement(key: string): Promise<void>\n reset(key: string): Promise<void>\n}\n\nexport interface RateLimitOptions {\n /** Maximum number of requests per window (default: 100) */\n max?: number\n /** Time window in milliseconds (default: 60_000) */\n windowMs?: number\n /** Response message when rate limit is exceeded (default: 'Too Many Requests') */\n message?: string\n /** HTTP status code when rate limit is exceeded (default: 429) */\n statusCode?: number\n /** Function to generate the key for rate limiting (default: req.ip) */\n keyGenerator?: (req: Request) => string\n /** Whether to send rate limit headers (default: true) */\n headers?: boolean\n /** Custom store implementation (default: in-memory Map) */\n store?: RateLimitStore\n /** Function to skip rate limiting for certain requests */\n skip?: (req: Request) => boolean\n /** Paths to exclude from rate limiting */\n skipPaths?: string[]\n}\n\ninterface MemoryStoreEntry {\n totalHits: number\n resetTime: Date\n}\n\nclass MemoryStore implements RateLimitStore {\n private hits = new Map<string, MemoryStoreEntry>()\n private cleanupTimer: ReturnType<typeof setInterval>\n\n constructor(private windowMs: number) {\n this.cleanupTimer = setInterval(() => this.cleanup(), windowMs)\n // Allow the process to exit without waiting for the timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref()\n }\n }\n\n async increment(key: string): Promise<{ totalHits: number; resetTime: Date }> {\n const now = Date.now()\n const entry = this.hits.get(key)\n\n if (entry && entry.resetTime.getTime() > now) {\n entry.totalHits++\n return { totalHits: entry.totalHits, resetTime: entry.resetTime }\n }\n\n const resetTime = new Date(now + this.windowMs)\n const newEntry: MemoryStoreEntry = { totalHits: 1, resetTime }\n this.hits.set(key, newEntry)\n return { totalHits: 1, resetTime }\n }\n\n async decrement(key: string): Promise<void> {\n const entry = this.hits.get(key)\n if (entry && entry.totalHits > 0) {\n entry.totalHits--\n }\n }\n\n async reset(key: string): Promise<void> {\n this.hits.delete(key)\n }\n\n private cleanup(): void {\n const now = Date.now()\n for (const [key, entry] of this.hits) {\n if (entry.resetTime.getTime() <= now) {\n this.hits.delete(key)\n }\n }\n }\n}\n\n/**\n * Rate limiting middleware.\n *\n * Limits the number of requests a client can make within a time window.\n * Uses an in-memory store by default, but accepts a custom store for\n * distributed deployments (e.g. Redis).\n *\n * @example\n * ```ts\n * import { rateLimit } from '@forinda/kickjs-http'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * rateLimit({ max: 100, windowMs: 60_000 }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function rateLimit(options: RateLimitOptions = {}) {\n const max = options.max ?? 100\n const windowMs = options.windowMs ?? 60_000\n const message = options.message ?? 'Too Many Requests'\n const statusCode = options.statusCode ?? 429\n const keyGenerator = options.keyGenerator ?? ((req: Request) => req.ip ?? '127.0.0.1')\n const sendHeaders = options.headers ?? true\n const store = options.store ?? new MemoryStore(windowMs)\n const skip = options.skip\n const skipPaths = new Set(options.skipPaths ?? [])\n\n return async (req: Request, res: Response, next: NextFunction) => {\n // Skip if path is in the skip list\n if (skipPaths.has(req.path)) {\n return next()\n }\n\n // Skip if the skip function returns true\n if (skip && skip(req)) {\n return next()\n }\n\n const key = keyGenerator(req)\n const { totalHits, resetTime } = await store.increment(key)\n const remaining = Math.max(0, max - totalHits)\n\n if (sendHeaders) {\n res.setHeader('RateLimit-Limit', max)\n res.setHeader('RateLimit-Remaining', remaining)\n res.setHeader('RateLimit-Reset', Math.ceil(resetTime.getTime() / 1000))\n }\n\n if (totalHits > max) {\n return res.status(statusCode).json({ message })\n }\n\n next()\n }\n}\n"],"mappings":";;;;;AAkCA,IAAMA,cAAN,MAAMA,aAAAA;EAAN,OAAMA;;;;EACIC,OAAO,oBAAIC,IAAAA;EACXC;EAER,YAAoBC,UAAkB;SAAlBA,WAAAA;AAClB,SAAKD,eAAeE,YAAY,MAAM,KAAKC,QAAO,GAAIF,QAAAA;AAEtD,QAAI,KAAKD,aAAaI,OAAO;AAC3B,WAAKJ,aAAaI,MAAK;IACzB;EACF;EAEA,MAAMC,UAAUC,KAA8D;AAC5E,UAAMC,MAAMC,KAAKD,IAAG;AACpB,UAAME,QAAQ,KAAKX,KAAKY,IAAIJ,GAAAA;AAE5B,QAAIG,SAASA,MAAME,UAAUC,QAAO,IAAKL,KAAK;AAC5CE,YAAMI;AACN,aAAO;QAAEA,WAAWJ,MAAMI;QAAWF,WAAWF,MAAME;MAAU;IAClE;AAEA,UAAMA,YAAY,IAAIH,KAAKD,MAAM,KAAKN,QAAQ;AAC9C,UAAMa,WAA6B;MAAED,WAAW;MAAGF;IAAU;AAC7D,SAAKb,KAAKiB,IAAIT,KAAKQ,QAAAA;AACnB,WAAO;MAAED,WAAW;MAAGF;IAAU;EACnC;EAEA,MAAMK,UAAUV,KAA4B;AAC1C,UAAMG,QAAQ,KAAKX,KAAKY,IAAIJ,GAAAA;AAC5B,QAAIG,SAASA,MAAMI,YAAY,GAAG;AAChCJ,YAAMI;IACR;EACF;EAEA,MAAMI,MAAMX,KAA4B;AACtC,SAAKR,KAAKoB,OAAOZ,GAAAA;EACnB;EAEQH,UAAgB;AACtB,UAAMI,MAAMC,KAAKD,IAAG;AACpB,eAAW,CAACD,KAAKG,KAAAA,KAAU,KAAKX,MAAM;AACpC,UAAIW,MAAME,UAAUC,QAAO,KAAML,KAAK;AACpC,aAAKT,KAAKoB,OAAOZ,GAAAA;MACnB;IACF;EACF;AACF;AAsBO,SAASa,UAAUC,UAA4B,CAAC,GAAC;AACtD,QAAMC,MAAMD,QAAQC,OAAO;AAC3B,QAAMpB,WAAWmB,QAAQnB,YAAY;AACrC,QAAMqB,UAAUF,QAAQE,WAAW;AACnC,QAAMC,aAAaH,QAAQG,cAAc;AACzC,QAAMC,eAAeJ,QAAQI,iBAAiB,CAACC,QAAiBA,IAAIC,MAAM;AAC1E,QAAMC,cAAcP,QAAQQ,WAAW;AACvC,QAAMC,QAAQT,QAAQS,SAAS,IAAIhC,YAAYI,QAAAA;AAC/C,QAAM6B,OAAOV,QAAQU;AACrB,QAAMC,YAAY,IAAIC,IAAIZ,QAAQW,aAAa,CAAA,CAAE;AAEjD,SAAO,OAAON,KAAcQ,KAAeC,SAAAA;AAEzC,QAAIH,UAAUI,IAAIV,IAAIW,IAAI,GAAG;AAC3B,aAAOF,KAAAA;IACT;AAGA,QAAIJ,QAAQA,KAAKL,GAAAA,GAAM;AACrB,aAAOS,KAAAA;IACT;AAEA,UAAM5B,MAAMkB,aAAaC,GAAAA;AACzB,UAAM,EAAEZ,WAAWF,UAAS,IAAK,MAAMkB,MAAMxB,UAAUC,GAAAA;AACvD,UAAM+B,YAAYC,KAAKjB,IAAI,GAAGA,MAAMR,SAAAA;AAEpC,QAAIc,aAAa;AACfM,UAAIM,UAAU,mBAAmBlB,GAAAA;AACjCY,UAAIM,UAAU,uBAAuBF,SAAAA;AACrCJ,UAAIM,UAAU,mBAAmBD,KAAKE,KAAK7B,UAAUC,QAAO,IAAK,GAAA,CAAA;IACnE;AAEA,QAAIC,YAAYQ,KAAK;AACnB,aAAOY,IAAIQ,OAAOlB,UAAAA,EAAYmB,KAAK;QAAEpB;MAAQ,CAAA;IAC/C;AAEAY,SAAAA;EACF;AACF;AAtCgBf;","names":["MemoryStore","hits","Map","cleanupTimer","windowMs","setInterval","cleanup","unref","increment","key","now","Date","entry","get","resetTime","getTime","totalHits","newEntry","set","decrement","reset","delete","rateLimit","options","max","message","statusCode","keyGenerator","req","ip","sendHeaders","headers","store","skip","skipPaths","Set","res","next","has","path","remaining","Math","setHeader","ceil","status","json"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__name
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/csrf.ts
|
|
6
6
|
import { randomBytes } from "crypto";
|
|
@@ -49,4 +49,4 @@ __name(csrf, "csrf");
|
|
|
49
49
|
export {
|
|
50
50
|
csrf
|
|
51
51
|
};
|
|
52
|
-
//# sourceMappingURL=chunk-
|
|
52
|
+
//# sourceMappingURL=chunk-I32MVBEG.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-WCQVDF3K.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware/upload.ts
|
|
6
|
+
import { unlink } from "fs/promises";
|
|
7
|
+
import multer from "multer";
|
|
8
|
+
var MIME_MAP = {
|
|
9
|
+
// Images
|
|
10
|
+
jpg: "image/jpeg",
|
|
11
|
+
jpeg: "image/jpeg",
|
|
12
|
+
png: "image/png",
|
|
13
|
+
gif: "image/gif",
|
|
14
|
+
webp: "image/webp",
|
|
15
|
+
svg: "image/svg+xml",
|
|
16
|
+
bmp: "image/bmp",
|
|
17
|
+
ico: "image/x-icon",
|
|
18
|
+
tiff: "image/tiff",
|
|
19
|
+
tif: "image/tiff",
|
|
20
|
+
avif: "image/avif",
|
|
21
|
+
// Documents
|
|
22
|
+
pdf: "application/pdf",
|
|
23
|
+
doc: "application/msword",
|
|
24
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
25
|
+
xls: "application/vnd.ms-excel",
|
|
26
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
27
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
28
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
29
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
30
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
31
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
32
|
+
rtf: "application/rtf",
|
|
33
|
+
txt: "text/plain",
|
|
34
|
+
csv: "text/csv",
|
|
35
|
+
// Archives
|
|
36
|
+
zip: "application/zip",
|
|
37
|
+
gz: "application/gzip",
|
|
38
|
+
tar: "application/x-tar",
|
|
39
|
+
rar: "application/vnd.rar",
|
|
40
|
+
"7z": "application/x-7z-compressed",
|
|
41
|
+
// Audio
|
|
42
|
+
mp3: "audio/mpeg",
|
|
43
|
+
wav: "audio/wav",
|
|
44
|
+
ogg: "audio/ogg",
|
|
45
|
+
flac: "audio/flac",
|
|
46
|
+
aac: "audio/aac",
|
|
47
|
+
// Video
|
|
48
|
+
mp4: "video/mp4",
|
|
49
|
+
webm: "video/webm",
|
|
50
|
+
avi: "video/x-msvideo",
|
|
51
|
+
mov: "video/quicktime",
|
|
52
|
+
mkv: "video/x-matroska",
|
|
53
|
+
// Other
|
|
54
|
+
json: "application/json",
|
|
55
|
+
xml: "application/xml",
|
|
56
|
+
html: "text/html"
|
|
57
|
+
};
|
|
58
|
+
function resolveMimeTypes(types) {
|
|
59
|
+
return types.map((t) => {
|
|
60
|
+
const lower = t.toLowerCase().replace(/^\./, "");
|
|
61
|
+
return MIME_MAP[lower] ?? t;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
__name(resolveMimeTypes, "resolveMimeTypes");
|
|
65
|
+
function createMulter(options) {
|
|
66
|
+
const mimeMap = options.customMimeMap ? {
|
|
67
|
+
...MIME_MAP,
|
|
68
|
+
...options.customMimeMap
|
|
69
|
+
} : MIME_MAP;
|
|
70
|
+
const limits = {
|
|
71
|
+
fileSize: options.maxSize ?? 5 * 1024 * 1024
|
|
72
|
+
};
|
|
73
|
+
let fileFilter;
|
|
74
|
+
if (typeof options.allowedTypes === "function") {
|
|
75
|
+
const filterFn = options.allowedTypes;
|
|
76
|
+
fileFilter = /* @__PURE__ */ __name((_req, file, cb) => {
|
|
77
|
+
if (filterFn(file.mimetype, file.originalname)) {
|
|
78
|
+
cb(null, true);
|
|
79
|
+
} else {
|
|
80
|
+
cb(new Error(`File type ${file.mimetype} is not allowed`));
|
|
81
|
+
}
|
|
82
|
+
}, "fileFilter");
|
|
83
|
+
} else if (Array.isArray(options.allowedTypes)) {
|
|
84
|
+
const resolvedTypes = options.allowedTypes.map((t) => {
|
|
85
|
+
const lower = t.toLowerCase().replace(/^\./, "");
|
|
86
|
+
return mimeMap[lower] ?? t;
|
|
87
|
+
});
|
|
88
|
+
fileFilter = /* @__PURE__ */ __name((_req, file, cb) => {
|
|
89
|
+
const allowed = resolvedTypes.some((type) => {
|
|
90
|
+
if (type.endsWith("/*")) {
|
|
91
|
+
return file.mimetype.startsWith(type.replace("/*", "/"));
|
|
92
|
+
}
|
|
93
|
+
return file.mimetype === type;
|
|
94
|
+
});
|
|
95
|
+
if (allowed) {
|
|
96
|
+
cb(null, true);
|
|
97
|
+
} else {
|
|
98
|
+
cb(new Error(`File type ${file.mimetype} is not allowed`));
|
|
99
|
+
}
|
|
100
|
+
}, "fileFilter");
|
|
101
|
+
}
|
|
102
|
+
const multerOptions = {
|
|
103
|
+
limits,
|
|
104
|
+
...fileFilter ? {
|
|
105
|
+
fileFilter
|
|
106
|
+
} : {},
|
|
107
|
+
...options.storage ? {
|
|
108
|
+
storage: options.storage
|
|
109
|
+
} : {},
|
|
110
|
+
...options.dest ? {
|
|
111
|
+
dest: options.dest
|
|
112
|
+
} : {}
|
|
113
|
+
};
|
|
114
|
+
return multer(multerOptions);
|
|
115
|
+
}
|
|
116
|
+
__name(createMulter, "createMulter");
|
|
117
|
+
function single(fieldName, options = {}) {
|
|
118
|
+
const m = createMulter(options);
|
|
119
|
+
return m.single(fieldName);
|
|
120
|
+
}
|
|
121
|
+
__name(single, "single");
|
|
122
|
+
function array(fieldName, maxCount = 10, options = {}) {
|
|
123
|
+
const m = createMulter(options);
|
|
124
|
+
return m.array(fieldName, maxCount);
|
|
125
|
+
}
|
|
126
|
+
__name(array, "array");
|
|
127
|
+
function none(options = {}) {
|
|
128
|
+
const m = createMulter(options);
|
|
129
|
+
return m.none();
|
|
130
|
+
}
|
|
131
|
+
__name(none, "none");
|
|
132
|
+
function cleanupFiles() {
|
|
133
|
+
return (req, res, next) => {
|
|
134
|
+
res.on("finish", async () => {
|
|
135
|
+
const files = [];
|
|
136
|
+
if (req.file?.path) {
|
|
137
|
+
files.push(req.file);
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(req.files)) {
|
|
140
|
+
for (const f of req.files) {
|
|
141
|
+
if (f?.path) files.push(f);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
try {
|
|
146
|
+
await unlink(file.path);
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
next();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
__name(cleanupFiles, "cleanupFiles");
|
|
155
|
+
function buildUploadMiddleware(config) {
|
|
156
|
+
const options = {};
|
|
157
|
+
if (config.maxSize) options.maxSize = config.maxSize;
|
|
158
|
+
if (config.allowedTypes) options.allowedTypes = config.allowedTypes;
|
|
159
|
+
if (config.customMimeMap) options.customMimeMap = config.customMimeMap;
|
|
160
|
+
const fieldName = config.fieldName ?? "file";
|
|
161
|
+
switch (config.mode) {
|
|
162
|
+
case "single":
|
|
163
|
+
return single(fieldName, options);
|
|
164
|
+
case "array":
|
|
165
|
+
return array(fieldName, config.maxCount ?? 10, options);
|
|
166
|
+
case "none":
|
|
167
|
+
return none(options);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
__name(buildUploadMiddleware, "buildUploadMiddleware");
|
|
171
|
+
var upload = {
|
|
172
|
+
single,
|
|
173
|
+
array,
|
|
174
|
+
none
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
resolveMimeTypes,
|
|
179
|
+
cleanupFiles,
|
|
180
|
+
buildUploadMiddleware,
|
|
181
|
+
upload
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=chunk-LEILPDMW.js.map
|