@mandujs/core 0.41.2 → 0.43.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.
Files changed (88) 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/event-bus.ts +2 -2
  64. package/src/observability/metrics.ts +334 -334
  65. package/src/observability/tracing.ts +694 -694
  66. package/src/openapi/generator.ts +1 -1
  67. package/src/perf/user-marks.ts +553 -553
  68. package/src/plugins/registry.ts +387 -387
  69. package/src/resource/ddl/diff.ts +392 -392
  70. package/src/resource/ddl/snapshot.ts +448 -447
  71. package/src/resource/generator-schema.ts +477 -476
  72. package/src/resource/parser.ts +4 -2
  73. package/src/resource/schema.ts +1 -1
  74. package/src/router/fs-patterns.ts +422 -422
  75. package/src/runtime/fast-refresh-types.ts +126 -128
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/router.test.ts +476 -476
  78. package/src/runtime/security.ts +155 -155
  79. package/src/runtime/server.ts +36 -19
  80. package/src/runtime/session-key.ts +328 -328
  81. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  82. package/src/seo/resolve/index.ts +353 -353
  83. package/src/spec/load.ts +1 -1
  84. package/src/testing/reporter.ts +676 -676
  85. package/src/testing/server.ts +196 -196
  86. package/src/testing/snapshot.ts +444 -444
  87. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  88. package/src/utils/bun.ts +8 -8
@@ -1,155 +1,155 @@
1
- /**
2
- * Runtime Security
3
- *
4
- * 동적 import 및 경로 접근 보안 검증
5
- */
6
-
7
- import path from "path";
8
- import type { Result } from "../error/result";
9
- import { ok, err } from "../error/result";
10
- import { SecurityError } from "../error/domains";
11
-
12
- /**
13
- * 허용된 import 경로 패턴
14
- */
15
- const ALLOWED_IMPORT_PATTERNS = [
16
- /^app\//, // app/ 디렉토리 (FS Routes)
17
- /^src\/client\//, // 클라이언트 코드
18
- /^src\/server\//, // 서버 코드
19
- /^src\/shared\//, // 공유 코드
20
- /^spec\//, // Spec 디렉토리 (레거시)
21
- ];
22
-
23
- /**
24
- * 허용된 파일 확장자
25
- */
26
- const ALLOWED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
27
-
28
- /**
29
- * 차단된 경로 패턴
30
- */
31
- const BLOCKED_PATTERNS = [
32
- /node_modules/, // node_modules 직접 접근 차단
33
- /\.env/, // 환경 변수 파일
34
- /\.git/, // Git 디렉토리
35
- /\.mandu\/.*\.json$/, // 설정 파일
36
- ];
37
-
38
- /**
39
- * 동적 import 경로 검증
40
- *
41
- * @param rootDir 프로젝트 루트 디렉토리
42
- * @param modulePath 상대 모듈 경로 (예: "app/layout.tsx")
43
- * @returns 검증된 전체 경로 또는 에러
44
- */
45
- export function validateImportPath(
46
- rootDir: string,
47
- modulePath: string
48
- ): Result<string> {
49
- // 1. 경로 정규화
50
- const normalized = path.posix.normalize(modulePath).replace(/\\/g, "/");
51
-
52
- // 2. Path traversal 체크
53
- if (normalized.includes("..")) {
54
- return err(
55
- new SecurityError(
56
- "path_traversal",
57
- `경로 탐색 공격 감지: ${modulePath}`,
58
- modulePath
59
- ).toManduError()
60
- );
61
- }
62
-
63
- // 3. 차단된 패턴 체크
64
- for (const pattern of BLOCKED_PATTERNS) {
65
- if (pattern.test(normalized)) {
66
- return err(
67
- new SecurityError(
68
- "import_violation",
69
- `차단된 경로 접근: ${modulePath}`,
70
- modulePath
71
- ).toManduError()
72
- );
73
- }
74
- }
75
-
76
- // 4. 화이트리스트 검증
77
- const isAllowed = ALLOWED_IMPORT_PATTERNS.some((pattern) =>
78
- pattern.test(normalized)
79
- );
80
-
81
- if (!isAllowed) {
82
- return err(
83
- new SecurityError(
84
- "import_violation",
85
- `허용되지 않은 import 경로: ${modulePath}. 허용된 경로: app/, src/client/, src/server/, src/shared/, spec/`,
86
- modulePath
87
- ).toManduError()
88
- );
89
- }
90
-
91
- // 5. 확장자 검증 (있는 경우만)
92
- const ext = path.extname(normalized);
93
- if (ext && !ALLOWED_EXTENSIONS.includes(ext)) {
94
- return err(
95
- new SecurityError(
96
- "import_violation",
97
- `허용되지 않은 파일 확장자: ${ext}`,
98
- modulePath
99
- ).toManduError()
100
- );
101
- }
102
-
103
- // 6. 전체 경로 생성
104
- const fullPath = path.join(rootDir, normalized);
105
-
106
- // 7. 최종 경로가 rootDir 내에 있는지 확인
107
- const resolvedPath = path.resolve(fullPath);
108
- const resolvedRoot = path.resolve(rootDir);
109
-
110
- if (!resolvedPath.startsWith(resolvedRoot + path.sep)) {
111
- return err(
112
- new SecurityError(
113
- "path_traversal",
114
- `루트 디렉토리 외부 접근 시도: ${modulePath}`,
115
- modulePath
116
- ).toManduError()
117
- );
118
- }
119
-
120
- return ok(fullPath);
121
- }
122
-
123
- /**
124
- * 안전한 동적 import
125
- *
126
- * @param rootDir 프로젝트 루트 디렉토리
127
- * @param modulePath 상대 모듈 경로
128
- * @returns 로드된 모듈 또는 null
129
- */
130
- export async function safeImport<T = unknown>(
131
- rootDir: string,
132
- modulePath: string
133
- ): Promise<T | null> {
134
- const validation = validateImportPath(rootDir, modulePath);
135
-
136
- if (!validation.ok) {
137
- console.error(`[Mandu Security] ${validation.error.message}`);
138
- return null;
139
- }
140
-
141
- try {
142
- const module = await import(validation.value);
143
- return module as T;
144
- } catch (error) {
145
- console.error(`[Mandu] Failed to import: ${modulePath}`, error);
146
- return null;
147
- }
148
- }
149
-
150
- /**
151
- * 모듈 경로 검증 (boolean 반환)
152
- */
153
- export function isValidImportPath(rootDir: string, modulePath: string): boolean {
154
- return validateImportPath(rootDir, modulePath).ok;
155
- }
1
+ /**
2
+ * Runtime Security
3
+ *
4
+ * 동적 import 및 경로 접근 보안 검증
5
+ */
6
+
7
+ import path from "path";
8
+ import type { Result } from "../error/result";
9
+ import { ok, err } from "../error/result";
10
+ import { SecurityError } from "../error/domains";
11
+
12
+ /**
13
+ * 허용된 import 경로 패턴
14
+ */
15
+ const ALLOWED_IMPORT_PATTERNS = [
16
+ /^app\//, // app/ 디렉토리 (FS Routes)
17
+ /^src\/client\//, // 클라이언트 코드
18
+ /^src\/server\//, // 서버 코드
19
+ /^src\/shared\//, // 공유 코드
20
+ /^spec\//, // Spec 디렉토리 (레거시)
21
+ ];
22
+
23
+ /**
24
+ * 허용된 파일 확장자
25
+ */
26
+ const ALLOWED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
27
+
28
+ /**
29
+ * 차단된 경로 패턴
30
+ */
31
+ const BLOCKED_PATTERNS = [
32
+ /node_modules/, // node_modules 직접 접근 차단
33
+ /\.env/, // 환경 변수 파일
34
+ /\.git/, // Git 디렉토리
35
+ /\.mandu\/.*\.json$/, // 설정 파일
36
+ ];
37
+
38
+ /**
39
+ * 동적 import 경로 검증
40
+ *
41
+ * @param rootDir 프로젝트 루트 디렉토리
42
+ * @param modulePath 상대 모듈 경로 (예: "app/layout.tsx")
43
+ * @returns 검증된 전체 경로 또는 에러
44
+ */
45
+ export function validateImportPath(
46
+ rootDir: string,
47
+ modulePath: string
48
+ ): Result<string> {
49
+ // 1. 경로 정규화
50
+ const normalized = path.posix.normalize(modulePath).replace(/\\/g, "/");
51
+
52
+ // 2. Path traversal 체크
53
+ if (normalized.includes("..")) {
54
+ return err(
55
+ new SecurityError(
56
+ "path_traversal",
57
+ `경로 탐색 공격 감지: ${modulePath}`,
58
+ modulePath
59
+ ).toManduError()
60
+ );
61
+ }
62
+
63
+ // 3. 차단된 패턴 체크
64
+ for (const pattern of BLOCKED_PATTERNS) {
65
+ if (pattern.test(normalized)) {
66
+ return err(
67
+ new SecurityError(
68
+ "import_violation",
69
+ `차단된 경로 접근: ${modulePath}`,
70
+ modulePath
71
+ ).toManduError()
72
+ );
73
+ }
74
+ }
75
+
76
+ // 4. 화이트리스트 검증
77
+ const isAllowed = ALLOWED_IMPORT_PATTERNS.some((pattern) =>
78
+ pattern.test(normalized)
79
+ );
80
+
81
+ if (!isAllowed) {
82
+ return err(
83
+ new SecurityError(
84
+ "import_violation",
85
+ `허용되지 않은 import 경로: ${modulePath}. 허용된 경로: app/, src/client/, src/server/, src/shared/, spec/`,
86
+ modulePath
87
+ ).toManduError()
88
+ );
89
+ }
90
+
91
+ // 5. 확장자 검증 (있는 경우만)
92
+ const ext = path.extname(normalized);
93
+ if (ext && !ALLOWED_EXTENSIONS.includes(ext)) {
94
+ return err(
95
+ new SecurityError(
96
+ "import_violation",
97
+ `허용되지 않은 파일 확장자: ${ext}`,
98
+ modulePath
99
+ ).toManduError()
100
+ );
101
+ }
102
+
103
+ // 6. 전체 경로 생성
104
+ const fullPath = path.join(rootDir, normalized);
105
+
106
+ // 7. 최종 경로가 rootDir 내에 있는지 확인
107
+ const resolvedPath = path.resolve(fullPath);
108
+ const resolvedRoot = path.resolve(rootDir);
109
+
110
+ if (!resolvedPath.startsWith(resolvedRoot + path.sep)) {
111
+ return err(
112
+ new SecurityError(
113
+ "path_traversal",
114
+ `루트 디렉토리 외부 접근 시도: ${modulePath}`,
115
+ modulePath
116
+ ).toManduError()
117
+ );
118
+ }
119
+
120
+ return ok(fullPath);
121
+ }
122
+
123
+ /**
124
+ * 안전한 동적 import
125
+ *
126
+ * @param rootDir 프로젝트 루트 디렉토리
127
+ * @param modulePath 상대 모듈 경로
128
+ * @returns 로드된 모듈 또는 null
129
+ */
130
+ export async function safeImport<T = unknown>(
131
+ rootDir: string,
132
+ modulePath: string
133
+ ): Promise<T | null> {
134
+ const validation = validateImportPath(rootDir, modulePath);
135
+
136
+ if (!validation.ok) {
137
+ console.error(`[Mandu Security] ${validation.error.message}`);
138
+ return null;
139
+ }
140
+
141
+ try {
142
+ const module = await import(validation.value);
143
+ return module as T;
144
+ } catch (error) {
145
+ console.error(`[Mandu] Failed to import: ${modulePath}`, error);
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * 모듈 경로 검증 (boolean 반환)
152
+ */
153
+ export function isValidImportPath(rootDir: string, modulePath: string): boolean {
154
+ return validateImportPath(rootDir, modulePath).ok;
155
+ }
@@ -1,4 +1,4 @@
1
- import type { Server } from "bun";
1
+ import type { Server, ServerWebSocket } from "bun";
2
2
  import type { RoutesManifest, RouteSpec, HydrationConfig, StaticParamSetSchema } from "../spec/schema";
3
3
  import type { BundleManifest } from "../bundler/types";
4
4
  import type { ManduFilling, RenderMode } from "../filling/filling";
@@ -77,8 +77,9 @@ import { collectPerfSnapshot } from "../perf/user-marks";
77
77
  // `startServer()`; `runWithSpan` is used at the absolute TOP of the
78
78
  // request handler so every downstream await (middleware, filling
79
79
  // loader, SSR render) inherits the active span via AsyncLocalStorage.
80
+ import type {
81
+ Tracer} from "../observability/tracing";
80
82
  import {
81
- Tracer,
82
83
  createTracerFromConfig,
83
84
  runWithSpan,
84
85
  setTracer,
@@ -4305,12 +4306,17 @@ function startBunServerWithFallback(options: {
4305
4306
  const { port: startPort, hostname, fetch, websocket } = options;
4306
4307
  let lastError: unknown = null;
4307
4308
 
4309
+ // `Parameters<typeof Bun.serve>[0]` captures the full `Serve.Options`
4310
+ // XOR union. We only ever use the hostname/port branch, so cast through
4311
+ // `unknown` at the call site to express "these are valid options we've
4312
+ // assembled dynamically" without widening the whole local to `any`.
4313
+ type BunServeOptions = Parameters<typeof Bun.serve>[0];
4308
4314
  const serveOptions: Record<string, unknown> = { hostname, fetch, idleTimeout: 255 };
4309
4315
  if (websocket) serveOptions.websocket = websocket;
4310
4316
 
4311
4317
  // Port 0: let Bun/OS pick an available ephemeral port.
4312
4318
  if (startPort === 0) {
4313
- const server = Bun.serve({ port: 0, ...serveOptions } as any);
4319
+ const server = Bun.serve({ port: 0, ...serveOptions } as unknown as BunServeOptions);
4314
4320
  return { server, port: server.port ?? 0, attempts: 0 };
4315
4321
  }
4316
4322
 
@@ -4320,7 +4326,7 @@ function startBunServerWithFallback(options: {
4320
4326
  continue;
4321
4327
  }
4322
4328
  try {
4323
- const server = Bun.serve({ port: candidate, ...serveOptions } as any);
4329
+ const server = Bun.serve({ port: candidate, ...serveOptions } as unknown as BunServeOptions);
4324
4330
  return { server, port: server.port ?? candidate, attempts: attempt };
4325
4331
  } catch (error) {
4326
4332
  if (!isPortInUseError(error)) {
@@ -4669,27 +4675,26 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
4669
4675
  handleRequest,
4670
4676
  });
4671
4677
 
4672
- // WebSocket 핸들러 빌드 (등록된 WS 라우트가 있을 때만)
4678
+ // WebSocket 핸들러 빌드 (등록된 WS 라우트가 있을 때만).
4679
+ // Bun's `ServerWebSocket<T>` carries the upgrade payload in `ws.data`;
4680
+ // we set `T = WSUpgradeData` at upgrade time (see `bunServer.upgrade`
4681
+ // below) so downstream handlers get proper inference without casts.
4673
4682
  const hasWsRoutes = registry.wsHandlers.size > 0;
4674
4683
  const wsConfig = hasWsRoutes ? {
4675
- open(ws: any) {
4676
- const data = ws.data as WSUpgradeData;
4677
- const handlers = registry.wsHandlers.get(data.routeId);
4684
+ open(ws: ServerWebSocket<WSUpgradeData>) {
4685
+ const handlers = registry.wsHandlers.get(ws.data.routeId);
4678
4686
  handlers?.open?.(wrapBunWebSocket(ws));
4679
4687
  },
4680
- message(ws: any, message: string | ArrayBuffer) {
4681
- const data = ws.data as WSUpgradeData;
4682
- const handlers = registry.wsHandlers.get(data.routeId);
4688
+ message(ws: ServerWebSocket<WSUpgradeData>, message: string | ArrayBuffer) {
4689
+ const handlers = registry.wsHandlers.get(ws.data.routeId);
4683
4690
  handlers?.message?.(wrapBunWebSocket(ws), message);
4684
4691
  },
4685
- close(ws: any, code: number, reason: string) {
4686
- const data = ws.data as WSUpgradeData;
4687
- const handlers = registry.wsHandlers.get(data.routeId);
4692
+ close(ws: ServerWebSocket<WSUpgradeData>, code: number, reason: string) {
4693
+ const handlers = registry.wsHandlers.get(ws.data.routeId);
4688
4694
  handlers?.close?.(wrapBunWebSocket(ws), code, reason);
4689
4695
  },
4690
- drain(ws: any) {
4691
- const data = ws.data as WSUpgradeData;
4692
- const handlers = registry.wsHandlers.get(data.routeId);
4696
+ drain(ws: ServerWebSocket<WSUpgradeData>) {
4697
+ const handlers = registry.wsHandlers.get(ws.data.routeId);
4693
4698
  handlers?.drain?.(wrapBunWebSocket(ws));
4694
4699
  },
4695
4700
  } : undefined;
@@ -4710,6 +4715,13 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
4710
4715
  };
4711
4716
 
4712
4717
  // fetch handler: WS upgrade 감지 추가
4718
+ //
4719
+ // Bun narrows `server.upgrade()` to require the matching WS data generic
4720
+ // (see `Server<T>` in bun-types/serve.d.ts:908). Our wsConfig is typed
4721
+ // against `WSUpgradeData`, so the server we receive at runtime is really
4722
+ // `Server<WSUpgradeData>`. The outer startBunServerWithFallback returns
4723
+ // `Server<undefined>` for back-compat; a single cast here threads the
4724
+ // correct generic through to `.upgrade()` without widening to `any`.
4713
4725
  const wrappedFetch = hasWsRoutes
4714
4726
  ? async (req: Request, bunServer: Server<undefined>): Promise<Response | undefined> => {
4715
4727
  // WebSocket upgrade 요청 감지
@@ -4717,7 +4729,8 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
4717
4729
  const url = new URL(req.url);
4718
4730
  const match = router.match(url.pathname);
4719
4731
  if (match && registry.wsHandlers.has(match.route.id)) {
4720
- const upgraded = (bunServer as any).upgrade(req, {
4732
+ const wsServer = bunServer as unknown as Server<WSUpgradeData>;
4733
+ const upgraded = wsServer.upgrade(req, {
4721
4734
  data: { routeId: match.route.id, params: match.params, id: newId() },
4722
4735
  });
4723
4736
  return upgraded ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
@@ -4736,7 +4749,11 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
4736
4749
  const { server, port: actualPort, attempts } = startBunServerWithFallback({
4737
4750
  port,
4738
4751
  hostname,
4739
- fetch: wrappedFetch as any,
4752
+ // `wrappedFetch` has a `Server<undefined>` signature but the WS-enabled
4753
+ // branch internally treats it as `Server<WSUpgradeData>` (see wsServer
4754
+ // cast above). The fallback helper accepts the narrower type; a cast
4755
+ // through `unknown` threads the WS-aware fetch without `any`.
4756
+ fetch: wrappedFetch as unknown as Parameters<typeof startBunServerWithFallback>[0]["fetch"],
4740
4757
  websocket: wsConfig,
4741
4758
  });
4742
4759