@mandujs/core 0.42.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/metrics.ts +334 -334
  64. package/src/observability/tracing.ts +694 -694
  65. package/src/openapi/generator.ts +1 -1
  66. package/src/perf/user-marks.ts +553 -553
  67. package/src/plugins/registry.ts +387 -387
  68. package/src/resource/ddl/diff.ts +392 -392
  69. package/src/resource/ddl/snapshot.ts +448 -447
  70. package/src/resource/generator-schema.ts +477 -476
  71. package/src/resource/parser.ts +4 -2
  72. package/src/resource/schema.ts +1 -1
  73. package/src/router/fs-patterns.ts +422 -422
  74. package/src/runtime/fast-refresh-types.ts +126 -128
  75. package/src/runtime/handlers.ts +300 -0
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/index.ts +4 -0
  78. package/src/runtime/router.test.ts +476 -476
  79. package/src/runtime/security.ts +155 -155
  80. package/src/runtime/server.ts +36 -19
  81. package/src/runtime/session-key.ts +328 -328
  82. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  83. package/src/seo/resolve/index.ts +353 -353
  84. package/src/spec/load.ts +1 -1
  85. package/src/testing/reporter.ts +676 -676
  86. package/src/testing/server.ts +196 -196
  87. package/src/testing/snapshot.ts +444 -444
  88. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  89. package/src/utils/bun.ts +8 -8
@@ -1,195 +1,206 @@
1
- /**
2
- * Mandu Image Handler
3
- * /_mandu/image?url=...&w=...&q=... 엔드포인트
4
- * 온디맨드 리사이즈 + WebP/AVIF 포맷 협상 + 캐시
5
- */
6
-
7
- import path from "path";
8
-
9
- // ========== Types ==========
10
-
11
- interface ImageOptions {
12
- width: number;
13
- quality: number;
14
- format: "webp" | "jpeg" | "png" | "avif";
15
- sourceContentType?: string;
16
- }
17
-
18
- interface ProcessedImageResult {
19
- data: Uint8Array;
20
- contentType: string;
21
- }
22
-
23
- // ========== Cache ==========
24
-
25
- const imageCache = new Map<string, { data: Uint8Array; contentType: string }>();
26
- const MAX_IMAGE_CACHE = 500;
27
-
28
- // ========== Handler ==========
29
-
30
- /**
31
- * 이미지 최적화 요청 처리
32
- * /_mandu/image?url=/photos/hero.jpg&w=800&q=80
33
- */
34
- export async function handleImageRequest(
35
- request: Request,
36
- rootDir: string,
37
- publicDir: string = "public"
38
- ): Promise<Response | null> {
39
- const url = new URL(request.url);
40
- if (url.pathname !== "/_mandu/image") return null;
41
-
42
- const src = url.searchParams.get("url");
43
- const width = Number(url.searchParams.get("w") ?? 800);
44
- const quality = Number(url.searchParams.get("q") ?? 80);
45
-
46
- if (!src || width < 1 || width > 4096 || quality < 1 || quality > 100) {
47
- return new Response("Invalid image parameters", { status: 400 });
48
- }
49
-
50
- // 보안: src가 /로 시작하고 traversal/null byte 없는지 확인
51
- if (!src.startsWith("/") || src.includes("..") || src.includes("\0")) {
52
- return new Response("Invalid image path", { status: 400 });
53
- }
54
-
55
- // 포맷 협상 (Accept 헤더 기반)
56
- const format = negotiateFormat(request);
57
- const cacheKey = `${src}:${width}:${quality}:${format}`;
58
-
59
- // 캐시 확인
60
- const cached = imageCache.get(cacheKey);
61
- if (cached) {
62
- return new Response(cached.data as unknown as BodyInit, {
63
- headers: {
64
- "Content-Type": cached.contentType,
65
- "Cache-Control": "public, max-age=31536000, immutable",
66
- "Vary": "Accept",
67
- "X-Mandu-Image-Cache": "HIT",
68
- },
69
- });
70
- }
71
-
72
- // 원본 파일 경로 해석 + symlink traversal 방지
73
- const allowedBaseDir = path.resolve(rootDir, publicDir);
74
- const filePath = path.join(allowedBaseDir, src.slice(1));
75
-
76
- // realpath로 symlink를 해석한 후 allowedBaseDir 내부인지 검증
77
- let resolvedPath: string;
78
- try {
79
- const realFs = require("fs") as typeof import("fs");
80
- resolvedPath = realFs.realpathSync(filePath);
81
- const resolvedBase = realFs.realpathSync(allowedBaseDir);
82
- if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
83
- return new Response("Forbidden", { status: 403 });
84
- }
85
- } catch {
86
- // realpath 실패 (broken symlink, 파일 없음) → 안전하게 404 반환
87
- return new Response("Image not found", { status: 404 });
88
- }
89
- const file = Bun.file(resolvedPath);
90
-
91
- if (!await file.exists()) {
92
- return new Response("Image not found", { status: 404 });
93
- }
94
-
95
- try {
96
- const original = await file.arrayBuffer();
97
- const processed = await processImage(new Uint8Array(original), {
98
- width,
99
- quality,
100
- format,
101
- sourceContentType: getMimeForExtension(src),
102
- });
103
-
104
- // 캐시 저장 (LRU)
105
- if (imageCache.size >= MAX_IMAGE_CACHE) {
106
- const oldest = imageCache.keys().next().value;
107
- if (oldest !== undefined) imageCache.delete(oldest);
108
- }
109
- imageCache.set(cacheKey, {
110
- data: processed.data,
111
- contentType: processed.contentType,
112
- });
113
-
114
- return new Response(processed.data as unknown as BodyInit, {
115
- headers: {
116
- "Content-Type": processed.contentType,
117
- "Cache-Control": "public, max-age=31536000, immutable",
118
- "Vary": "Accept",
119
- },
120
- });
121
- } catch (error) {
122
- console.error(`[Mandu Image] Processing failed for ${src}:`, error);
123
- // 원본 파일 그대로 반환 (fallback)
124
- return new Response(file, {
125
- headers: {
126
- "Content-Type": getMimeForExtension(src),
127
- "Cache-Control": "public, max-age=86400",
128
- },
129
- });
130
- }
131
- }
132
-
133
- // ========== Format Negotiation ==========
134
-
135
- function negotiateFormat(request: Request): "webp" | "jpeg" | "png" | "avif" {
136
- const accept = request.headers.get("Accept") ?? "";
137
- if (accept.includes("image/avif")) return "avif";
138
- if (accept.includes("image/webp")) return "webp";
139
- return "jpeg";
140
- }
141
-
142
- function getMimeForExtension(src: string): string {
143
- const ext = path.extname(src).toLowerCase();
144
- const map: Record<string, string> = {
145
- ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
146
- ".png": "image/png", ".gif": "image/gif",
147
- ".webp": "image/webp", ".avif": "image/avif",
148
- ".svg": "image/svg+xml",
149
- };
150
- return map[ext] ?? "application/octet-stream";
151
- }
152
-
153
- // ========== Image Processing ==========
154
-
155
- /**
156
- * 이미지 리사이즈 + 포맷 변환
157
- * Bun의 내장 sharp 미지원 시 원본 반환 (graceful degradation)
158
- */
159
- async function processImage(
160
- data: Uint8Array,
161
- options: ImageOptions
162
- ): Promise<ProcessedImageResult> {
163
- // sharp 사용 시도 (선택적 의존성)
164
- try {
165
- const sharp = require("sharp") as any;
166
- let pipeline = sharp(Buffer.from(data)).resize(options.width);
167
-
168
- switch (options.format) {
169
- case "webp":
170
- pipeline = pipeline.webp({ quality: options.quality });
171
- break;
172
- case "avif":
173
- pipeline = pipeline.avif({ quality: options.quality });
174
- break;
175
- case "jpeg":
176
- pipeline = pipeline.jpeg({ quality: options.quality });
177
- break;
178
- case "png":
179
- pipeline = pipeline.png({ quality: options.quality });
180
- break;
181
- }
182
-
183
- const result = await pipeline.toBuffer();
184
- return {
185
- data: new Uint8Array(result),
186
- contentType: `image/${options.format}`,
187
- };
188
- } catch {
189
- // sharp 미설치 시 원본 반환
190
- return {
191
- data,
192
- contentType: options.sourceContentType ?? "application/octet-stream",
193
- };
194
- }
195
- }
1
+ /**
2
+ * Mandu Image Handler
3
+ * /_mandu/image?url=...&w=...&q=... 엔드포인트
4
+ * 온디맨드 리사이즈 + WebP/AVIF 포맷 협상 + 캐시
5
+ */
6
+
7
+ import path from "path";
8
+
9
+ // ========== Types ==========
10
+
11
+ interface ImageOptions {
12
+ width: number;
13
+ quality: number;
14
+ format: "webp" | "jpeg" | "png" | "avif";
15
+ sourceContentType?: string;
16
+ }
17
+
18
+ interface ProcessedImageResult {
19
+ data: Uint8Array;
20
+ contentType: string;
21
+ }
22
+
23
+ // ========== Cache ==========
24
+
25
+ const imageCache = new Map<string, { data: Uint8Array; contentType: string }>();
26
+ const MAX_IMAGE_CACHE = 500;
27
+
28
+ // ========== Handler ==========
29
+
30
+ /**
31
+ * 이미지 최적화 요청 처리
32
+ * /_mandu/image?url=/photos/hero.jpg&w=800&q=80
33
+ */
34
+ export async function handleImageRequest(
35
+ request: Request,
36
+ rootDir: string,
37
+ publicDir: string = "public"
38
+ ): Promise<Response | null> {
39
+ const url = new URL(request.url);
40
+ if (url.pathname !== "/_mandu/image") return null;
41
+
42
+ const src = url.searchParams.get("url");
43
+ const width = Number(url.searchParams.get("w") ?? 800);
44
+ const quality = Number(url.searchParams.get("q") ?? 80);
45
+
46
+ if (!src || width < 1 || width > 4096 || quality < 1 || quality > 100) {
47
+ return new Response("Invalid image parameters", { status: 400 });
48
+ }
49
+
50
+ // 보안: src가 /로 시작하고 traversal/null byte 없는지 확인
51
+ if (!src.startsWith("/") || src.includes("..") || src.includes("\0")) {
52
+ return new Response("Invalid image path", { status: 400 });
53
+ }
54
+
55
+ // 포맷 협상 (Accept 헤더 기반)
56
+ const format = negotiateFormat(request);
57
+ const cacheKey = `${src}:${width}:${quality}:${format}`;
58
+
59
+ // 캐시 확인
60
+ const cached = imageCache.get(cacheKey);
61
+ if (cached) {
62
+ return new Response(cached.data as unknown as BodyInit, {
63
+ headers: {
64
+ "Content-Type": cached.contentType,
65
+ "Cache-Control": "public, max-age=31536000, immutable",
66
+ "Vary": "Accept",
67
+ "X-Mandu-Image-Cache": "HIT",
68
+ },
69
+ });
70
+ }
71
+
72
+ // 원본 파일 경로 해석 + symlink traversal 방지
73
+ const allowedBaseDir = path.resolve(rootDir, publicDir);
74
+ const filePath = path.join(allowedBaseDir, src.slice(1));
75
+
76
+ // realpath로 symlink를 해석한 후 allowedBaseDir 내부인지 검증
77
+ let resolvedPath: string;
78
+ try {
79
+ const realFs = require("fs") as typeof import("fs");
80
+ resolvedPath = realFs.realpathSync(filePath);
81
+ const resolvedBase = realFs.realpathSync(allowedBaseDir);
82
+ if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
83
+ return new Response("Forbidden", { status: 403 });
84
+ }
85
+ } catch {
86
+ // realpath 실패 (broken symlink, 파일 없음) → 안전하게 404 반환
87
+ return new Response("Image not found", { status: 404 });
88
+ }
89
+ const file = Bun.file(resolvedPath);
90
+
91
+ if (!await file.exists()) {
92
+ return new Response("Image not found", { status: 404 });
93
+ }
94
+
95
+ try {
96
+ const original = await file.arrayBuffer();
97
+ const processed = await processImage(new Uint8Array(original), {
98
+ width,
99
+ quality,
100
+ format,
101
+ sourceContentType: getMimeForExtension(src),
102
+ });
103
+
104
+ // 캐시 저장 (LRU)
105
+ if (imageCache.size >= MAX_IMAGE_CACHE) {
106
+ const oldest = imageCache.keys().next().value;
107
+ if (oldest !== undefined) imageCache.delete(oldest);
108
+ }
109
+ imageCache.set(cacheKey, {
110
+ data: processed.data,
111
+ contentType: processed.contentType,
112
+ });
113
+
114
+ return new Response(processed.data as unknown as BodyInit, {
115
+ headers: {
116
+ "Content-Type": processed.contentType,
117
+ "Cache-Control": "public, max-age=31536000, immutable",
118
+ "Vary": "Accept",
119
+ },
120
+ });
121
+ } catch (error) {
122
+ console.error(`[Mandu Image] Processing failed for ${src}:`, error);
123
+ // 원본 파일 그대로 반환 (fallback)
124
+ return new Response(file, {
125
+ headers: {
126
+ "Content-Type": getMimeForExtension(src),
127
+ "Cache-Control": "public, max-age=86400",
128
+ },
129
+ });
130
+ }
131
+ }
132
+
133
+ // ========== Format Negotiation ==========
134
+
135
+ function negotiateFormat(request: Request): "webp" | "jpeg" | "png" | "avif" {
136
+ const accept = request.headers.get("Accept") ?? "";
137
+ if (accept.includes("image/avif")) return "avif";
138
+ if (accept.includes("image/webp")) return "webp";
139
+ return "jpeg";
140
+ }
141
+
142
+ function getMimeForExtension(src: string): string {
143
+ const ext = path.extname(src).toLowerCase();
144
+ const map: Record<string, string> = {
145
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
146
+ ".png": "image/png", ".gif": "image/gif",
147
+ ".webp": "image/webp", ".avif": "image/avif",
148
+ ".svg": "image/svg+xml",
149
+ };
150
+ return map[ext] ?? "application/octet-stream";
151
+ }
152
+
153
+ // ========== Image Processing ==========
154
+
155
+ /**
156
+ * 이미지 리사이즈 + 포맷 변환
157
+ * Bun의 내장 sharp 미지원 시 원본 반환 (graceful degradation)
158
+ */
159
+ async function processImage(
160
+ data: Uint8Array,
161
+ options: ImageOptions
162
+ ): Promise<ProcessedImageResult> {
163
+ // sharp 사용 시도 (선택적 의존성)
164
+ try {
165
+ // sharp is a heavyweight optional native dep. Type it minimally to the
166
+ // tiny surface we actually call — `resize/webp/avif/jpeg/png/toBuffer`.
167
+ interface SharpPipeline {
168
+ resize(width: number): SharpPipeline;
169
+ webp(opts: { quality: number }): SharpPipeline;
170
+ avif(opts: { quality: number }): SharpPipeline;
171
+ jpeg(opts: { quality: number }): SharpPipeline;
172
+ png(opts: { quality: number }): SharpPipeline;
173
+ toBuffer(): Promise<Buffer>;
174
+ }
175
+ type SharpFn = (input: Buffer) => SharpPipeline;
176
+ const sharp = require("sharp") as SharpFn;
177
+ let pipeline = sharp(Buffer.from(data)).resize(options.width);
178
+
179
+ switch (options.format) {
180
+ case "webp":
181
+ pipeline = pipeline.webp({ quality: options.quality });
182
+ break;
183
+ case "avif":
184
+ pipeline = pipeline.avif({ quality: options.quality });
185
+ break;
186
+ case "jpeg":
187
+ pipeline = pipeline.jpeg({ quality: options.quality });
188
+ break;
189
+ case "png":
190
+ pipeline = pipeline.png({ quality: options.quality });
191
+ break;
192
+ }
193
+
194
+ const result = await pipeline.toBuffer();
195
+ return {
196
+ data: new Uint8Array(result),
197
+ contentType: `image/${options.format}`,
198
+ };
199
+ } catch {
200
+ // sharp 미설치 시 원본 반환
201
+ return {
202
+ data,
203
+ contentType: options.sourceContentType ?? "application/octet-stream",
204
+ };
205
+ }
206
+ }
@@ -3,6 +3,10 @@ export * from "./streaming-ssr";
3
3
  export { extractShellHtml, createPPRResponse, PPR_SHELL_MARKER } from "./ppr";
4
4
  export * from "./router";
5
5
  export * from "./server";
6
+ export {
7
+ registerManifestHandlers,
8
+ type RegisterHandlersOptions,
9
+ } from "./handlers";
6
10
  export { redirect, isManduRedirect, isRedirectResponse, REDIRECT_BRAND } from "./redirect";
7
11
  export type { RedirectStatus, RedirectOptions } from "./redirect";
8
12
  export { notFound, isNotFoundResponse, NOT_FOUND_BRAND } from "./not-found";