@mandujs/core 0.13.0 → 0.13.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.
- package/README.ko.md +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +9 -0
- package/src/config/validate.ts +12 -0
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
package/src/runtime/server.ts
CHANGED
|
@@ -29,6 +29,183 @@ import {
|
|
|
29
29
|
} from "./cors";
|
|
30
30
|
import { validateImportPath } from "./security";
|
|
31
31
|
|
|
32
|
+
export interface RateLimitOptions {
|
|
33
|
+
windowMs?: number;
|
|
34
|
+
max?: number;
|
|
35
|
+
message?: string;
|
|
36
|
+
statusCode?: number;
|
|
37
|
+
headers?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Reverse proxy 헤더를 신뢰할지 여부
|
|
40
|
+
* - false(기본): X-Forwarded-For 등을 읽지만 spoofing 가능성을 표시
|
|
41
|
+
* - true: 전달된 클라이언트 IP를 완전히 신뢰
|
|
42
|
+
* 주의: trustProxy: false여도 클라이언트 구분을 위해 헤더를 사용하므로
|
|
43
|
+
* IP spoofing이 가능합니다. 신뢰할 수 있는 프록시 뒤에서만 사용하세요.
|
|
44
|
+
*/
|
|
45
|
+
trustProxy?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* 메모리 보호를 위한 최대 key 수
|
|
48
|
+
* - 초과 시 오래된 key부터 제거
|
|
49
|
+
*/
|
|
50
|
+
maxKeys?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NormalizedRateLimitOptions {
|
|
54
|
+
windowMs: number;
|
|
55
|
+
max: number;
|
|
56
|
+
message: string;
|
|
57
|
+
statusCode: number;
|
|
58
|
+
headers: boolean;
|
|
59
|
+
trustProxy: boolean;
|
|
60
|
+
maxKeys: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RateLimitDecision {
|
|
64
|
+
allowed: boolean;
|
|
65
|
+
limit: number;
|
|
66
|
+
remaining: number;
|
|
67
|
+
resetAt: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class MemoryRateLimiter {
|
|
71
|
+
private readonly store = new Map<string, { count: number; resetAt: number }>();
|
|
72
|
+
private lastCleanupAt = 0;
|
|
73
|
+
|
|
74
|
+
consume(req: Request, routeId: string, options: NormalizedRateLimitOptions): RateLimitDecision {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
this.maybeCleanup(now, options);
|
|
77
|
+
|
|
78
|
+
const key = `${this.getClientKey(req, options)}:${routeId}`;
|
|
79
|
+
const current = this.store.get(key);
|
|
80
|
+
|
|
81
|
+
if (!current || current.resetAt <= now) {
|
|
82
|
+
const resetAt = now + options.windowMs;
|
|
83
|
+
this.store.set(key, { count: 1, resetAt });
|
|
84
|
+
this.enforceMaxKeys(options.maxKeys);
|
|
85
|
+
return { allowed: true, limit: options.max, remaining: Math.max(0, options.max - 1), resetAt };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
current.count += 1;
|
|
89
|
+
this.store.set(key, current);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
allowed: current.count <= options.max,
|
|
93
|
+
limit: options.max,
|
|
94
|
+
remaining: Math.max(0, options.max - current.count),
|
|
95
|
+
resetAt: current.resetAt,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private maybeCleanup(now: number, options: NormalizedRateLimitOptions): void {
|
|
100
|
+
if (now - this.lastCleanupAt < Math.max(1_000, options.windowMs)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.lastCleanupAt = now;
|
|
105
|
+
for (const [key, entry] of this.store.entries()) {
|
|
106
|
+
if (entry.resetAt <= now) {
|
|
107
|
+
this.store.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private enforceMaxKeys(maxKeys: number): void {
|
|
113
|
+
while (this.store.size > maxKeys) {
|
|
114
|
+
const oldestKey = this.store.keys().next().value;
|
|
115
|
+
if (!oldestKey) break;
|
|
116
|
+
this.store.delete(oldestKey);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private getClientKey(req: Request, options: NormalizedRateLimitOptions): string {
|
|
121
|
+
const candidates = [
|
|
122
|
+
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
|
|
123
|
+
req.headers.get("x-real-ip")?.trim(),
|
|
124
|
+
req.headers.get("cf-connecting-ip")?.trim(),
|
|
125
|
+
req.headers.get("true-client-ip")?.trim(),
|
|
126
|
+
req.headers.get("fly-client-ip")?.trim(),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
if (candidate) {
|
|
131
|
+
const sanitized = candidate.slice(0, 64);
|
|
132
|
+
// trustProxy: false면 경고를 위해 prefix 추가 (spoofing 가능)
|
|
133
|
+
return options.trustProxy ? sanitized : `unverified:${sanitized}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 헤더가 전혀 없는 경우만 fallback (로컬 개발 환경)
|
|
138
|
+
return "default";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeRateLimitOptions(options: boolean | RateLimitOptions | undefined): NormalizedRateLimitOptions | false {
|
|
143
|
+
if (!options) return false;
|
|
144
|
+
if (options === true) {
|
|
145
|
+
return {
|
|
146
|
+
windowMs: 60_000,
|
|
147
|
+
max: 100,
|
|
148
|
+
message: "Too Many Requests",
|
|
149
|
+
statusCode: 429,
|
|
150
|
+
headers: true,
|
|
151
|
+
trustProxy: false,
|
|
152
|
+
maxKeys: 10_000,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const windowMs = Number.isFinite(options.windowMs) ? Math.max(1_000, options.windowMs!) : 60_000;
|
|
157
|
+
const max = Number.isFinite(options.max) ? Math.max(1, Math.floor(options.max!)) : 100;
|
|
158
|
+
const statusCode = Number.isFinite(options.statusCode)
|
|
159
|
+
? Math.min(599, Math.max(400, Math.floor(options.statusCode!)))
|
|
160
|
+
: 429;
|
|
161
|
+
const maxKeys = Number.isFinite(options.maxKeys)
|
|
162
|
+
? Math.max(100, Math.floor(options.maxKeys!))
|
|
163
|
+
: 10_000;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
windowMs,
|
|
167
|
+
max,
|
|
168
|
+
message: options.message ?? "Too Many Requests",
|
|
169
|
+
statusCode,
|
|
170
|
+
headers: options.headers ?? true,
|
|
171
|
+
trustProxy: options.trustProxy ?? false,
|
|
172
|
+
maxKeys,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function appendRateLimitHeaders(response: Response, decision: RateLimitDecision, options: NormalizedRateLimitOptions): Response {
|
|
177
|
+
if (!options.headers) return response;
|
|
178
|
+
|
|
179
|
+
const headers = new Headers(response.headers);
|
|
180
|
+
const retryAfterSec = Math.max(1, Math.ceil((decision.resetAt - Date.now()) / 1000));
|
|
181
|
+
|
|
182
|
+
headers.set("X-RateLimit-Limit", String(decision.limit));
|
|
183
|
+
headers.set("X-RateLimit-Remaining", String(decision.remaining));
|
|
184
|
+
headers.set("X-RateLimit-Reset", String(Math.floor(decision.resetAt / 1000)));
|
|
185
|
+
headers.set("Retry-After", String(retryAfterSec));
|
|
186
|
+
|
|
187
|
+
return new Response(response.body, {
|
|
188
|
+
status: response.status,
|
|
189
|
+
statusText: response.statusText,
|
|
190
|
+
headers,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function createRateLimitResponse(decision: RateLimitDecision, options: NormalizedRateLimitOptions): Response {
|
|
195
|
+
const response = Response.json(
|
|
196
|
+
{
|
|
197
|
+
error: "rate_limit_exceeded",
|
|
198
|
+
message: options.message,
|
|
199
|
+
limit: decision.limit,
|
|
200
|
+
remaining: decision.remaining,
|
|
201
|
+
retryAfter: Math.max(1, Math.ceil((decision.resetAt - Date.now()) / 1000)),
|
|
202
|
+
},
|
|
203
|
+
{ status: options.statusCode }
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return appendRateLimitHeaders(response, decision, options);
|
|
207
|
+
}
|
|
208
|
+
|
|
32
209
|
// ========== MIME Types ==========
|
|
33
210
|
const MIME_TYPES: Record<string, string> = {
|
|
34
211
|
// JavaScript
|
|
@@ -107,6 +284,10 @@ export interface ServerOptions {
|
|
|
107
284
|
* - false: 기존 renderToString 사용 (기본값)
|
|
108
285
|
*/
|
|
109
286
|
streaming?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* API 라우트 Rate Limit 설정
|
|
289
|
+
*/
|
|
290
|
+
rateLimit?: boolean | RateLimitOptions;
|
|
110
291
|
/**
|
|
111
292
|
* CSS 파일 경로 (SSR 링크 주입용)
|
|
112
293
|
* - string: 해당 경로로 <link> 주입 (예: "/.mandu/client/globals.css")
|
|
@@ -203,6 +384,7 @@ export interface ServerRegistrySettings {
|
|
|
203
384
|
publicDir: string;
|
|
204
385
|
cors?: CorsOptions | false;
|
|
205
386
|
streaming: boolean;
|
|
387
|
+
rateLimit?: NormalizedRateLimitOptions | false;
|
|
206
388
|
/**
|
|
207
389
|
* CSS 파일 경로 (SSR 링크 주입용)
|
|
208
390
|
* - string: 해당 경로로 <link> 주입
|
|
@@ -230,12 +412,14 @@ export class ServerRegistry {
|
|
|
230
412
|
/** Error 로더 (모듈 경로 → 로더 함수) */
|
|
231
413
|
readonly errorLoaders: Map<string, ErrorLoader> = new Map();
|
|
232
414
|
createAppFn: CreateAppFn | null = null;
|
|
415
|
+
rateLimiter: MemoryRateLimiter | null = null;
|
|
233
416
|
settings: ServerRegistrySettings = {
|
|
234
417
|
isDev: false,
|
|
235
418
|
rootDir: process.cwd(),
|
|
236
419
|
publicDir: "public",
|
|
237
420
|
cors: false,
|
|
238
421
|
streaming: false,
|
|
422
|
+
rateLimit: false,
|
|
239
423
|
};
|
|
240
424
|
|
|
241
425
|
registerApiHandler(routeId: string, handler: ApiHandler): void {
|
|
@@ -375,6 +559,7 @@ export class ServerRegistry {
|
|
|
375
559
|
this.errorComponents.clear();
|
|
376
560
|
this.errorLoaders.clear();
|
|
377
561
|
this.createAppFn = null;
|
|
562
|
+
this.rateLimiter = null;
|
|
378
563
|
}
|
|
379
564
|
}
|
|
380
565
|
|
|
@@ -923,6 +1108,18 @@ async function handleRequestInternal(
|
|
|
923
1108
|
|
|
924
1109
|
// 3. 라우트 종류별 처리
|
|
925
1110
|
if (route.kind === "api") {
|
|
1111
|
+
const rateLimitOptions = settings.rateLimit;
|
|
1112
|
+
if (rateLimitOptions && registry.rateLimiter) {
|
|
1113
|
+
const decision = registry.rateLimiter.consume(req, route.id, rateLimitOptions);
|
|
1114
|
+
if (!decision.allowed) {
|
|
1115
|
+
return ok(createRateLimitResponse(decision, rateLimitOptions));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const apiResult = await handleApiRoute(req, route, params, registry);
|
|
1119
|
+
if (!apiResult.ok) return apiResult;
|
|
1120
|
+
return ok(appendRateLimitHeaders(apiResult.value, decision, rateLimitOptions));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
926
1123
|
return handleApiRoute(req, route, params, registry);
|
|
927
1124
|
}
|
|
928
1125
|
|
|
@@ -1011,6 +1208,7 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
1011
1208
|
publicDir = "public",
|
|
1012
1209
|
cors = false,
|
|
1013
1210
|
streaming = false,
|
|
1211
|
+
rateLimit = false,
|
|
1014
1212
|
cssPath: cssPathOption,
|
|
1015
1213
|
registry = defaultRegistry,
|
|
1016
1214
|
} = options;
|
|
@@ -1027,6 +1225,7 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
1027
1225
|
|
|
1028
1226
|
// CORS 옵션 파싱
|
|
1029
1227
|
const corsOptions: CorsOptions | false = cors === true ? {} : cors;
|
|
1228
|
+
const rateLimitOptions = normalizeRateLimitOptions(rateLimit);
|
|
1030
1229
|
|
|
1031
1230
|
if (!isDev && cors === true) {
|
|
1032
1231
|
console.warn("⚠️ [Security Warning] CORS is set to allow all origins.");
|
|
@@ -1044,9 +1243,12 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
1044
1243
|
publicDir,
|
|
1045
1244
|
cors: corsOptions,
|
|
1046
1245
|
streaming,
|
|
1246
|
+
rateLimit: rateLimitOptions,
|
|
1047
1247
|
cssPath,
|
|
1048
1248
|
};
|
|
1049
1249
|
|
|
1250
|
+
registry.rateLimiter = rateLimitOptions ? new MemoryRateLimiter() : null;
|
|
1251
|
+
|
|
1050
1252
|
const router = new Router(manifest.routes);
|
|
1051
1253
|
|
|
1052
1254
|
// Fetch handler with CORS support (registry를 클로저로 캡처)
|
|
@@ -1112,3 +1314,58 @@ export const apiHandlers = defaultRegistry.apiHandlers;
|
|
|
1112
1314
|
export const pageLoaders = defaultRegistry.pageLoaders;
|
|
1113
1315
|
export const pageHandlers = defaultRegistry.pageHandlers;
|
|
1114
1316
|
export const routeComponents = defaultRegistry.routeComponents;
|
|
1317
|
+
|
|
1318
|
+
// ========== Rate Limiting Public API ==========
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Rate limiter 인스턴스 생성
|
|
1322
|
+
* API 핸들러에서 직접 사용 가능
|
|
1323
|
+
*
|
|
1324
|
+
* @example
|
|
1325
|
+
* ```typescript
|
|
1326
|
+
* import { createRateLimiter } from '@mandujs/core/runtime/server';
|
|
1327
|
+
*
|
|
1328
|
+
* const limiter = createRateLimiter({ max: 5, windowMs: 60000 });
|
|
1329
|
+
*
|
|
1330
|
+
* export async function POST(req: Request) {
|
|
1331
|
+
* const decision = limiter.check(req, 'my-api-route');
|
|
1332
|
+
* if (!decision.allowed) {
|
|
1333
|
+
* return limiter.createResponse(decision);
|
|
1334
|
+
* }
|
|
1335
|
+
* // ... 정상 로직
|
|
1336
|
+
* }
|
|
1337
|
+
* ```
|
|
1338
|
+
*/
|
|
1339
|
+
export function createRateLimiter(options?: RateLimitOptions) {
|
|
1340
|
+
const normalized = normalizeRateLimitOptions(options || true);
|
|
1341
|
+
if (!normalized) {
|
|
1342
|
+
throw new Error('Rate limiter options cannot be false');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const limiter = new MemoryRateLimiter();
|
|
1346
|
+
|
|
1347
|
+
return {
|
|
1348
|
+
/**
|
|
1349
|
+
* Rate limit 체크
|
|
1350
|
+
* @param req Request 객체 (IP 추출용)
|
|
1351
|
+
* @param routeId 라우트 식별자 (동일 IP라도 라우트별로 독립적인 limit)
|
|
1352
|
+
*/
|
|
1353
|
+
check(req: Request, routeId: string): RateLimitDecision {
|
|
1354
|
+
return limiter.consume(req, routeId, normalized);
|
|
1355
|
+
},
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Rate limit 초과 시 429 응답 생성
|
|
1359
|
+
*/
|
|
1360
|
+
createResponse(decision: RateLimitDecision): Response {
|
|
1361
|
+
return createRateLimitResponse(decision, normalized);
|
|
1362
|
+
},
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* 정상 응답에 Rate limit 헤더 추가
|
|
1366
|
+
*/
|
|
1367
|
+
addHeaders(response: Response, decision: RateLimitDecision): Response {
|
|
1368
|
+
return appendRateLimitHeaders(response, decision, normalized);
|
|
1369
|
+
},
|
|
1370
|
+
};
|
|
1371
|
+
}
|