@mandujs/core 0.12.1 → 0.13.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.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +8 -8
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- 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 +94 -96
- package/src/config/validate.ts +213 -215
- 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/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- 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 +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/healing.ts +2 -0
- package/src/guard/index.ts +2 -0
- package/src/guard/negotiation.ts +430 -4
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +175 -0
- package/src/guard/presets/cqrs.ts +107 -0
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -288
- 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 -352
- package/src/guard/types.ts +348 -347
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- 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/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- 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 +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- 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/watcher/rules.ts +5 -5
|
@@ -1,363 +1,363 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Kitchen DevTools - Network Proxy
|
|
3
|
-
* @version 1.0.3
|
|
4
|
-
*
|
|
5
|
-
* Fetch/XHR 요청을 인터셉트하여 DevTools로 전달
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { NetworkRequest, NetworkBodyPolicy } from '../../types';
|
|
9
|
-
import { getOrCreateHook } from '../../hook';
|
|
10
|
-
import { createNetworkRequestEvent, createNetworkResponseEvent, ALLOWED_HEADERS, BLOCKED_HEADERS } from '../../protocol';
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// Types
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
interface NetworkProxyOptions {
|
|
17
|
-
/** Network body 수집 정책 */
|
|
18
|
-
bodyPolicy?: NetworkBodyPolicy;
|
|
19
|
-
/** 무시할 URL 패턴 */
|
|
20
|
-
ignorePatterns?: (string | RegExp)[];
|
|
21
|
-
/** 최대 추적 요청 수 */
|
|
22
|
-
maxTrackedRequests?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ============================================================================
|
|
26
|
-
// Constants
|
|
27
|
-
// ============================================================================
|
|
28
|
-
|
|
29
|
-
const DEFAULT_OPTIONS: Required<NetworkProxyOptions> = {
|
|
30
|
-
bodyPolicy: {
|
|
31
|
-
collectBody: false,
|
|
32
|
-
optInPolicy: {
|
|
33
|
-
maxBytes: 10_000,
|
|
34
|
-
applyPIIFilter: true,
|
|
35
|
-
applySecretFilter: true,
|
|
36
|
-
allowedContentTypes: ['application/json', 'text/plain', 'text/event-stream'],
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
ignorePatterns: [
|
|
40
|
-
// DevTools 자체 요청
|
|
41
|
-
/__mandu/,
|
|
42
|
-
// HMR
|
|
43
|
-
/__vite/,
|
|
44
|
-
/\.hot-update\./,
|
|
45
|
-
// Source maps
|
|
46
|
-
/\.map$/,
|
|
47
|
-
// Chrome extensions
|
|
48
|
-
/^chrome-extension:/,
|
|
49
|
-
],
|
|
50
|
-
maxTrackedRequests: 200,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// ============================================================================
|
|
54
|
-
// Helper Functions
|
|
55
|
-
// ============================================================================
|
|
56
|
-
|
|
57
|
-
let requestIdCounter = 0;
|
|
58
|
-
|
|
59
|
-
function generateRequestId(): string {
|
|
60
|
-
return `req-${Date.now()}-${++requestIdCounter}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function shouldIgnore(url: string, patterns: (string | RegExp)[]): boolean {
|
|
64
|
-
for (const pattern of patterns) {
|
|
65
|
-
if (typeof pattern === 'string') {
|
|
66
|
-
if (url.includes(pattern)) return true;
|
|
67
|
-
} else {
|
|
68
|
-
if (pattern.test(url)) return true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function extractSafeHeaders(headers: Headers | Record<string, string>): {
|
|
75
|
-
safeHeaders: Record<string, string>;
|
|
76
|
-
redactedHeaders: string[];
|
|
77
|
-
} {
|
|
78
|
-
const safeHeaders: Record<string, string> = {};
|
|
79
|
-
const redactedHeaders: string[] = [];
|
|
80
|
-
|
|
81
|
-
const entries = headers instanceof Headers
|
|
82
|
-
? Array.from(headers.entries())
|
|
83
|
-
: Object.entries(headers);
|
|
84
|
-
|
|
85
|
-
for (const [key, value] of entries) {
|
|
86
|
-
const lowerKey = key.toLowerCase();
|
|
87
|
-
|
|
88
|
-
if (BLOCKED_HEADERS.has(lowerKey)) {
|
|
89
|
-
redactedHeaders.push(key);
|
|
90
|
-
} else if (ALLOWED_HEADERS.has(lowerKey)) {
|
|
91
|
-
safeHeaders[key] = value;
|
|
92
|
-
} else {
|
|
93
|
-
// 커스텀 헤더: 키만 표시
|
|
94
|
-
safeHeaders[key] = '[...]';
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { safeHeaders, redactedHeaders };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function isStreamingResponse(contentType: string | null): boolean {
|
|
102
|
-
if (!contentType) return false;
|
|
103
|
-
return (
|
|
104
|
-
contentType.includes('text/event-stream') ||
|
|
105
|
-
contentType.includes('application/x-ndjson')
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ============================================================================
|
|
110
|
-
// Network Proxy Class
|
|
111
|
-
// ============================================================================
|
|
112
|
-
|
|
113
|
-
export class NetworkProxy {
|
|
114
|
-
private options: Required<NetworkProxyOptions>;
|
|
115
|
-
private isAttached = false;
|
|
116
|
-
private originalFetch?: typeof fetch;
|
|
117
|
-
private trackedRequests = new Map<string, NetworkRequest>();
|
|
118
|
-
|
|
119
|
-
constructor(options?: NetworkProxyOptions) {
|
|
120
|
-
this.options = {
|
|
121
|
-
...DEFAULT_OPTIONS,
|
|
122
|
-
...options,
|
|
123
|
-
bodyPolicy: {
|
|
124
|
-
...DEFAULT_OPTIONS.bodyPolicy,
|
|
125
|
-
...options?.bodyPolicy,
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// --------------------------------------------------------------------------
|
|
131
|
-
// Lifecycle
|
|
132
|
-
// --------------------------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
attach(): void {
|
|
135
|
-
if (this.isAttached || typeof window === 'undefined') return;
|
|
136
|
-
|
|
137
|
-
this.attachFetch();
|
|
138
|
-
this.isAttached = true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
detach(): void {
|
|
142
|
-
if (!this.isAttached || typeof window === 'undefined') return;
|
|
143
|
-
|
|
144
|
-
if (this.originalFetch) {
|
|
145
|
-
window.fetch = this.originalFetch;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this.trackedRequests.clear();
|
|
149
|
-
this.isAttached = false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// --------------------------------------------------------------------------
|
|
153
|
-
// Fetch Interceptor
|
|
154
|
-
// --------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
private attachFetch(): void {
|
|
157
|
-
this.originalFetch = window.fetch.bind(window);
|
|
158
|
-
const self = this;
|
|
159
|
-
const originalFetch = this.originalFetch;
|
|
160
|
-
|
|
161
|
-
(window as any).fetch = async function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
162
|
-
const url = typeof input === 'string'
|
|
163
|
-
? input
|
|
164
|
-
: input instanceof URL
|
|
165
|
-
? input.href
|
|
166
|
-
: input.url;
|
|
167
|
-
|
|
168
|
-
// 무시 패턴 체크
|
|
169
|
-
if (shouldIgnore(url, self.options.ignorePatterns)) {
|
|
170
|
-
return originalFetch(input, init);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const requestId = generateRequestId();
|
|
174
|
-
const method = init?.method ?? 'GET';
|
|
175
|
-
const headers = new Headers(init?.headers);
|
|
176
|
-
const { safeHeaders, redactedHeaders } = extractSafeHeaders(headers);
|
|
177
|
-
|
|
178
|
-
// 요청 시작 이벤트
|
|
179
|
-
const request: Omit<NetworkRequest, 'id' | 'startTime'> = {
|
|
180
|
-
method: method.toUpperCase(),
|
|
181
|
-
url,
|
|
182
|
-
safeHeaders,
|
|
183
|
-
redactedHeaders,
|
|
184
|
-
isStreaming: false,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const event = createNetworkRequestEvent(request);
|
|
188
|
-
const trackedRequest = event.data as NetworkRequest;
|
|
189
|
-
self.trackedRequests.set(requestId, trackedRequest);
|
|
190
|
-
|
|
191
|
-
// 최대 추적 수 제한
|
|
192
|
-
if (self.trackedRequests.size > self.options.maxTrackedRequests) {
|
|
193
|
-
const firstKey = self.trackedRequests.keys().next().value;
|
|
194
|
-
if (firstKey) self.trackedRequests.delete(firstKey);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
getOrCreateHook().emit(event);
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
const response = await originalFetch(input, init);
|
|
201
|
-
|
|
202
|
-
// 스트리밍 여부 확인
|
|
203
|
-
const contentType = response.headers.get('content-type');
|
|
204
|
-
const isStreaming = isStreamingResponse(contentType);
|
|
205
|
-
|
|
206
|
-
if (isStreaming) {
|
|
207
|
-
// SSE/스트리밍 응답 처리
|
|
208
|
-
return self.handleStreamingResponse(requestId, response);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 일반 응답 완료 이벤트
|
|
212
|
-
const responseEvent = createNetworkResponseEvent(requestId, response.status);
|
|
213
|
-
getOrCreateHook().emit(responseEvent);
|
|
214
|
-
|
|
215
|
-
return response;
|
|
216
|
-
} catch (error) {
|
|
217
|
-
// 네트워크 에러
|
|
218
|
-
getOrCreateHook().emit({
|
|
219
|
-
type: 'network:error',
|
|
220
|
-
timestamp: Date.now(),
|
|
221
|
-
data: {
|
|
222
|
-
id: requestId,
|
|
223
|
-
error: error instanceof Error ? error.message : String(error),
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// --------------------------------------------------------------------------
|
|
233
|
-
// Streaming Response Handler
|
|
234
|
-
// --------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
private handleStreamingResponse(requestId: string, response: Response): Response {
|
|
237
|
-
const trackedRequest = this.trackedRequests.get(requestId);
|
|
238
|
-
if (trackedRequest) {
|
|
239
|
-
trackedRequest.isStreaming = true;
|
|
240
|
-
trackedRequest.chunkCount = 0;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 응답 복제 (body를 두 번 읽기 위해)
|
|
244
|
-
const clonedResponse = response.clone();
|
|
245
|
-
const reader = clonedResponse.body?.getReader();
|
|
246
|
-
|
|
247
|
-
if (reader) {
|
|
248
|
-
this.trackStreamChunks(requestId, reader);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return response;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private async trackStreamChunks(
|
|
255
|
-
requestId: string,
|
|
256
|
-
reader: ReadableStreamDefaultReader<Uint8Array>
|
|
257
|
-
): Promise<void> {
|
|
258
|
-
const trackedRequest = this.trackedRequests.get(requestId);
|
|
259
|
-
let chunkIndex = 0;
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
while (true) {
|
|
263
|
-
const { done, value } = await reader.read();
|
|
264
|
-
|
|
265
|
-
if (done) {
|
|
266
|
-
// 스트림 완료
|
|
267
|
-
const responseEvent = createNetworkResponseEvent(requestId, 200);
|
|
268
|
-
getOrCreateHook().emit(responseEvent);
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Chunk 이벤트
|
|
273
|
-
if (trackedRequest) {
|
|
274
|
-
trackedRequest.chunkCount = (trackedRequest.chunkCount ?? 0) + 1;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
getOrCreateHook().emit({
|
|
278
|
-
type: 'network:chunk',
|
|
279
|
-
timestamp: Date.now(),
|
|
280
|
-
data: {
|
|
281
|
-
id: requestId,
|
|
282
|
-
chunkIndex: chunkIndex++,
|
|
283
|
-
size: value?.length ?? 0,
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
getOrCreateHook().emit({
|
|
289
|
-
type: 'network:error',
|
|
290
|
-
timestamp: Date.now(),
|
|
291
|
-
data: {
|
|
292
|
-
id: requestId,
|
|
293
|
-
error: error instanceof Error ? error.message : String(error),
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// --------------------------------------------------------------------------
|
|
300
|
-
// Manual Tracking
|
|
301
|
-
// --------------------------------------------------------------------------
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* 수동으로 요청 추적 (외부 라이브러리 등)
|
|
305
|
-
*/
|
|
306
|
-
trackRequest(
|
|
307
|
-
method: string,
|
|
308
|
-
url: string,
|
|
309
|
-
headers?: Record<string, string>
|
|
310
|
-
): string {
|
|
311
|
-
const requestId = generateRequestId();
|
|
312
|
-
const { safeHeaders, redactedHeaders } = extractSafeHeaders(headers ?? {});
|
|
313
|
-
|
|
314
|
-
const event = createNetworkRequestEvent({
|
|
315
|
-
method: method.toUpperCase(),
|
|
316
|
-
url,
|
|
317
|
-
safeHeaders,
|
|
318
|
-
redactedHeaders,
|
|
319
|
-
isStreaming: false,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const trackedRequest = event.data as NetworkRequest;
|
|
323
|
-
this.trackedRequests.set(requestId, trackedRequest);
|
|
324
|
-
getOrCreateHook().emit(event);
|
|
325
|
-
|
|
326
|
-
return requestId;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* 수동으로 응답 완료 알림
|
|
331
|
-
*/
|
|
332
|
-
completeRequest(requestId: string, status: number): void {
|
|
333
|
-
const event = createNetworkResponseEvent(requestId, status);
|
|
334
|
-
getOrCreateHook().emit(event);
|
|
335
|
-
this.trackedRequests.delete(requestId);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ============================================================================
|
|
340
|
-
// Singleton Instance
|
|
341
|
-
// ============================================================================
|
|
342
|
-
|
|
343
|
-
let globalNetworkProxy: NetworkProxy | null = null;
|
|
344
|
-
|
|
345
|
-
export function getNetworkProxy(options?: NetworkProxyOptions): NetworkProxy {
|
|
346
|
-
if (!globalNetworkProxy) {
|
|
347
|
-
globalNetworkProxy = new NetworkProxy(options);
|
|
348
|
-
}
|
|
349
|
-
return globalNetworkProxy;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function initializeNetworkProxy(options?: NetworkProxyOptions): NetworkProxy {
|
|
353
|
-
const proxy = getNetworkProxy(options);
|
|
354
|
-
proxy.attach();
|
|
355
|
-
return proxy;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export function destroyNetworkProxy(): void {
|
|
359
|
-
if (globalNetworkProxy) {
|
|
360
|
-
globalNetworkProxy.detach();
|
|
361
|
-
globalNetworkProxy = null;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Kitchen DevTools - Network Proxy
|
|
3
|
+
* @version 1.0.3
|
|
4
|
+
*
|
|
5
|
+
* Fetch/XHR 요청을 인터셉트하여 DevTools로 전달
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { NetworkRequest, NetworkBodyPolicy } from '../../types';
|
|
9
|
+
import { getOrCreateHook } from '../../hook';
|
|
10
|
+
import { createNetworkRequestEvent, createNetworkResponseEvent, ALLOWED_HEADERS, BLOCKED_HEADERS } from '../../protocol';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
interface NetworkProxyOptions {
|
|
17
|
+
/** Network body 수집 정책 */
|
|
18
|
+
bodyPolicy?: NetworkBodyPolicy;
|
|
19
|
+
/** 무시할 URL 패턴 */
|
|
20
|
+
ignorePatterns?: (string | RegExp)[];
|
|
21
|
+
/** 최대 추적 요청 수 */
|
|
22
|
+
maxTrackedRequests?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Constants
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const DEFAULT_OPTIONS: Required<NetworkProxyOptions> = {
|
|
30
|
+
bodyPolicy: {
|
|
31
|
+
collectBody: false,
|
|
32
|
+
optInPolicy: {
|
|
33
|
+
maxBytes: 10_000,
|
|
34
|
+
applyPIIFilter: true,
|
|
35
|
+
applySecretFilter: true,
|
|
36
|
+
allowedContentTypes: ['application/json', 'text/plain', 'text/event-stream'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
ignorePatterns: [
|
|
40
|
+
// DevTools 자체 요청
|
|
41
|
+
/__mandu/,
|
|
42
|
+
// HMR
|
|
43
|
+
/__vite/,
|
|
44
|
+
/\.hot-update\./,
|
|
45
|
+
// Source maps
|
|
46
|
+
/\.map$/,
|
|
47
|
+
// Chrome extensions
|
|
48
|
+
/^chrome-extension:/,
|
|
49
|
+
],
|
|
50
|
+
maxTrackedRequests: 200,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Helper Functions
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
let requestIdCounter = 0;
|
|
58
|
+
|
|
59
|
+
function generateRequestId(): string {
|
|
60
|
+
return `req-${Date.now()}-${++requestIdCounter}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shouldIgnore(url: string, patterns: (string | RegExp)[]): boolean {
|
|
64
|
+
for (const pattern of patterns) {
|
|
65
|
+
if (typeof pattern === 'string') {
|
|
66
|
+
if (url.includes(pattern)) return true;
|
|
67
|
+
} else {
|
|
68
|
+
if (pattern.test(url)) return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractSafeHeaders(headers: Headers | Record<string, string>): {
|
|
75
|
+
safeHeaders: Record<string, string>;
|
|
76
|
+
redactedHeaders: string[];
|
|
77
|
+
} {
|
|
78
|
+
const safeHeaders: Record<string, string> = {};
|
|
79
|
+
const redactedHeaders: string[] = [];
|
|
80
|
+
|
|
81
|
+
const entries = headers instanceof Headers
|
|
82
|
+
? Array.from(headers.entries())
|
|
83
|
+
: Object.entries(headers);
|
|
84
|
+
|
|
85
|
+
for (const [key, value] of entries) {
|
|
86
|
+
const lowerKey = key.toLowerCase();
|
|
87
|
+
|
|
88
|
+
if (BLOCKED_HEADERS.has(lowerKey)) {
|
|
89
|
+
redactedHeaders.push(key);
|
|
90
|
+
} else if (ALLOWED_HEADERS.has(lowerKey)) {
|
|
91
|
+
safeHeaders[key] = value;
|
|
92
|
+
} else {
|
|
93
|
+
// 커스텀 헤더: 키만 표시
|
|
94
|
+
safeHeaders[key] = '[...]';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { safeHeaders, redactedHeaders };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isStreamingResponse(contentType: string | null): boolean {
|
|
102
|
+
if (!contentType) return false;
|
|
103
|
+
return (
|
|
104
|
+
contentType.includes('text/event-stream') ||
|
|
105
|
+
contentType.includes('application/x-ndjson')
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Network Proxy Class
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
export class NetworkProxy {
|
|
114
|
+
private options: Required<NetworkProxyOptions>;
|
|
115
|
+
private isAttached = false;
|
|
116
|
+
private originalFetch?: typeof fetch;
|
|
117
|
+
private trackedRequests = new Map<string, NetworkRequest>();
|
|
118
|
+
|
|
119
|
+
constructor(options?: NetworkProxyOptions) {
|
|
120
|
+
this.options = {
|
|
121
|
+
...DEFAULT_OPTIONS,
|
|
122
|
+
...options,
|
|
123
|
+
bodyPolicy: {
|
|
124
|
+
...DEFAULT_OPTIONS.bodyPolicy,
|
|
125
|
+
...options?.bodyPolicy,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --------------------------------------------------------------------------
|
|
131
|
+
// Lifecycle
|
|
132
|
+
// --------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
attach(): void {
|
|
135
|
+
if (this.isAttached || typeof window === 'undefined') return;
|
|
136
|
+
|
|
137
|
+
this.attachFetch();
|
|
138
|
+
this.isAttached = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
detach(): void {
|
|
142
|
+
if (!this.isAttached || typeof window === 'undefined') return;
|
|
143
|
+
|
|
144
|
+
if (this.originalFetch) {
|
|
145
|
+
window.fetch = this.originalFetch;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.trackedRequests.clear();
|
|
149
|
+
this.isAttached = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --------------------------------------------------------------------------
|
|
153
|
+
// Fetch Interceptor
|
|
154
|
+
// --------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
private attachFetch(): void {
|
|
157
|
+
this.originalFetch = window.fetch.bind(window);
|
|
158
|
+
const self = this;
|
|
159
|
+
const originalFetch = this.originalFetch;
|
|
160
|
+
|
|
161
|
+
(window as any).fetch = async function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
162
|
+
const url = typeof input === 'string'
|
|
163
|
+
? input
|
|
164
|
+
: input instanceof URL
|
|
165
|
+
? input.href
|
|
166
|
+
: input.url;
|
|
167
|
+
|
|
168
|
+
// 무시 패턴 체크
|
|
169
|
+
if (shouldIgnore(url, self.options.ignorePatterns)) {
|
|
170
|
+
return originalFetch(input, init);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const requestId = generateRequestId();
|
|
174
|
+
const method = init?.method ?? 'GET';
|
|
175
|
+
const headers = new Headers(init?.headers);
|
|
176
|
+
const { safeHeaders, redactedHeaders } = extractSafeHeaders(headers);
|
|
177
|
+
|
|
178
|
+
// 요청 시작 이벤트
|
|
179
|
+
const request: Omit<NetworkRequest, 'id' | 'startTime'> = {
|
|
180
|
+
method: method.toUpperCase(),
|
|
181
|
+
url,
|
|
182
|
+
safeHeaders,
|
|
183
|
+
redactedHeaders,
|
|
184
|
+
isStreaming: false,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const event = createNetworkRequestEvent(request);
|
|
188
|
+
const trackedRequest = event.data as NetworkRequest;
|
|
189
|
+
self.trackedRequests.set(requestId, trackedRequest);
|
|
190
|
+
|
|
191
|
+
// 최대 추적 수 제한
|
|
192
|
+
if (self.trackedRequests.size > self.options.maxTrackedRequests) {
|
|
193
|
+
const firstKey = self.trackedRequests.keys().next().value;
|
|
194
|
+
if (firstKey) self.trackedRequests.delete(firstKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
getOrCreateHook().emit(event);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await originalFetch(input, init);
|
|
201
|
+
|
|
202
|
+
// 스트리밍 여부 확인
|
|
203
|
+
const contentType = response.headers.get('content-type');
|
|
204
|
+
const isStreaming = isStreamingResponse(contentType);
|
|
205
|
+
|
|
206
|
+
if (isStreaming) {
|
|
207
|
+
// SSE/스트리밍 응답 처리
|
|
208
|
+
return self.handleStreamingResponse(requestId, response);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 일반 응답 완료 이벤트
|
|
212
|
+
const responseEvent = createNetworkResponseEvent(requestId, response.status);
|
|
213
|
+
getOrCreateHook().emit(responseEvent);
|
|
214
|
+
|
|
215
|
+
return response;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// 네트워크 에러
|
|
218
|
+
getOrCreateHook().emit({
|
|
219
|
+
type: 'network:error',
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
data: {
|
|
222
|
+
id: requestId,
|
|
223
|
+
error: error instanceof Error ? error.message : String(error),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --------------------------------------------------------------------------
|
|
233
|
+
// Streaming Response Handler
|
|
234
|
+
// --------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
private handleStreamingResponse(requestId: string, response: Response): Response {
|
|
237
|
+
const trackedRequest = this.trackedRequests.get(requestId);
|
|
238
|
+
if (trackedRequest) {
|
|
239
|
+
trackedRequest.isStreaming = true;
|
|
240
|
+
trackedRequest.chunkCount = 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 응답 복제 (body를 두 번 읽기 위해)
|
|
244
|
+
const clonedResponse = response.clone();
|
|
245
|
+
const reader = clonedResponse.body?.getReader();
|
|
246
|
+
|
|
247
|
+
if (reader) {
|
|
248
|
+
this.trackStreamChunks(requestId, reader);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return response;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private async trackStreamChunks(
|
|
255
|
+
requestId: string,
|
|
256
|
+
reader: ReadableStreamDefaultReader<Uint8Array>
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const trackedRequest = this.trackedRequests.get(requestId);
|
|
259
|
+
let chunkIndex = 0;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
while (true) {
|
|
263
|
+
const { done, value } = await reader.read();
|
|
264
|
+
|
|
265
|
+
if (done) {
|
|
266
|
+
// 스트림 완료
|
|
267
|
+
const responseEvent = createNetworkResponseEvent(requestId, 200);
|
|
268
|
+
getOrCreateHook().emit(responseEvent);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Chunk 이벤트
|
|
273
|
+
if (trackedRequest) {
|
|
274
|
+
trackedRequest.chunkCount = (trackedRequest.chunkCount ?? 0) + 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getOrCreateHook().emit({
|
|
278
|
+
type: 'network:chunk',
|
|
279
|
+
timestamp: Date.now(),
|
|
280
|
+
data: {
|
|
281
|
+
id: requestId,
|
|
282
|
+
chunkIndex: chunkIndex++,
|
|
283
|
+
size: value?.length ?? 0,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
getOrCreateHook().emit({
|
|
289
|
+
type: 'network:error',
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
data: {
|
|
292
|
+
id: requestId,
|
|
293
|
+
error: error instanceof Error ? error.message : String(error),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --------------------------------------------------------------------------
|
|
300
|
+
// Manual Tracking
|
|
301
|
+
// --------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 수동으로 요청 추적 (외부 라이브러리 등)
|
|
305
|
+
*/
|
|
306
|
+
trackRequest(
|
|
307
|
+
method: string,
|
|
308
|
+
url: string,
|
|
309
|
+
headers?: Record<string, string>
|
|
310
|
+
): string {
|
|
311
|
+
const requestId = generateRequestId();
|
|
312
|
+
const { safeHeaders, redactedHeaders } = extractSafeHeaders(headers ?? {});
|
|
313
|
+
|
|
314
|
+
const event = createNetworkRequestEvent({
|
|
315
|
+
method: method.toUpperCase(),
|
|
316
|
+
url,
|
|
317
|
+
safeHeaders,
|
|
318
|
+
redactedHeaders,
|
|
319
|
+
isStreaming: false,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const trackedRequest = event.data as NetworkRequest;
|
|
323
|
+
this.trackedRequests.set(requestId, trackedRequest);
|
|
324
|
+
getOrCreateHook().emit(event);
|
|
325
|
+
|
|
326
|
+
return requestId;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 수동으로 응답 완료 알림
|
|
331
|
+
*/
|
|
332
|
+
completeRequest(requestId: string, status: number): void {
|
|
333
|
+
const event = createNetworkResponseEvent(requestId, status);
|
|
334
|
+
getOrCreateHook().emit(event);
|
|
335
|
+
this.trackedRequests.delete(requestId);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// Singleton Instance
|
|
341
|
+
// ============================================================================
|
|
342
|
+
|
|
343
|
+
let globalNetworkProxy: NetworkProxy | null = null;
|
|
344
|
+
|
|
345
|
+
export function getNetworkProxy(options?: NetworkProxyOptions): NetworkProxy {
|
|
346
|
+
if (!globalNetworkProxy) {
|
|
347
|
+
globalNetworkProxy = new NetworkProxy(options);
|
|
348
|
+
}
|
|
349
|
+
return globalNetworkProxy;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function initializeNetworkProxy(options?: NetworkProxyOptions): NetworkProxy {
|
|
353
|
+
const proxy = getNetworkProxy(options);
|
|
354
|
+
proxy.attach();
|
|
355
|
+
return proxy;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function destroyNetworkProxy(): void {
|
|
359
|
+
if (globalNetworkProxy) {
|
|
360
|
+
globalNetworkProxy.detach();
|
|
361
|
+
globalNetworkProxy = null;
|
|
362
|
+
}
|
|
363
|
+
}
|