@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.
Files changed (177) hide show
  1. package/README.ko.md +304 -304
  2. package/README.md +653 -653
  3. package/package.json +8 -8
  4. package/src/brain/architecture/analyzer.ts +28 -26
  5. package/src/brain/doctor/analyzer.ts +1 -1
  6. package/src/bundler/build.ts +91 -91
  7. package/src/bundler/css.ts +302 -302
  8. package/src/bundler/dev.ts +0 -1
  9. package/src/change/history.ts +3 -3
  10. package/src/change/snapshot.ts +10 -9
  11. package/src/change/transaction.ts +2 -2
  12. package/src/client/Link.tsx +227 -227
  13. package/src/client/globals.ts +44 -44
  14. package/src/client/hooks.ts +267 -267
  15. package/src/client/index.ts +5 -5
  16. package/src/client/island.ts +8 -8
  17. package/src/client/router.ts +435 -435
  18. package/src/client/runtime.ts +23 -23
  19. package/src/client/serialize.ts +404 -404
  20. package/src/client/window-state.ts +101 -101
  21. package/src/config/mandu.ts +94 -96
  22. package/src/config/validate.ts +213 -215
  23. package/src/config/watcher.ts +311 -311
  24. package/src/constants.ts +40 -40
  25. package/src/content/content-layer.ts +314 -314
  26. package/src/content/content.test.ts +433 -433
  27. package/src/content/data-store.ts +245 -245
  28. package/src/content/digest.ts +133 -133
  29. package/src/content/index.ts +164 -164
  30. package/src/content/loader-context.ts +172 -172
  31. package/src/content/loaders/api.ts +216 -216
  32. package/src/content/loaders/file.ts +169 -169
  33. package/src/content/loaders/glob.ts +252 -252
  34. package/src/content/loaders/index.ts +34 -34
  35. package/src/content/loaders/types.ts +137 -137
  36. package/src/content/meta-store.ts +209 -209
  37. package/src/content/types.ts +282 -282
  38. package/src/content/watcher.ts +135 -135
  39. package/src/contract/client-safe.test.ts +42 -42
  40. package/src/contract/client-safe.ts +114 -114
  41. package/src/contract/client.ts +16 -16
  42. package/src/contract/define.ts +459 -459
  43. package/src/contract/handler.ts +10 -10
  44. package/src/contract/normalize.test.ts +276 -276
  45. package/src/contract/normalize.ts +404 -404
  46. package/src/contract/registry.test.ts +206 -206
  47. package/src/contract/registry.ts +568 -568
  48. package/src/contract/schema.ts +48 -48
  49. package/src/contract/types.ts +58 -58
  50. package/src/contract/validator.ts +32 -32
  51. package/src/devtools/ai/context-builder.ts +375 -375
  52. package/src/devtools/ai/index.ts +25 -25
  53. package/src/devtools/ai/mcp-connector.ts +465 -465
  54. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  55. package/src/devtools/client/catchers/index.ts +18 -18
  56. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  57. package/src/devtools/client/components/index.ts +39 -39
  58. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  59. package/src/devtools/client/components/mandu-character.tsx +241 -241
  60. package/src/devtools/client/components/overlay.tsx +368 -368
  61. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  62. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  63. package/src/devtools/client/components/panel/index.ts +32 -32
  64. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  65. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  66. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  67. package/src/devtools/client/filters/context-filters.ts +282 -282
  68. package/src/devtools/client/filters/index.ts +16 -16
  69. package/src/devtools/client/index.ts +63 -63
  70. package/src/devtools/client/persistence.ts +335 -335
  71. package/src/devtools/client/state-manager.ts +478 -478
  72. package/src/devtools/design-tokens.ts +263 -263
  73. package/src/devtools/hook/create-hook.ts +207 -207
  74. package/src/devtools/hook/index.ts +13 -13
  75. package/src/devtools/index.ts +439 -439
  76. package/src/devtools/init.ts +266 -266
  77. package/src/devtools/protocol.ts +237 -237
  78. package/src/devtools/server/index.ts +17 -17
  79. package/src/devtools/server/source-context.ts +444 -444
  80. package/src/devtools/types.ts +319 -319
  81. package/src/devtools/worker/index.ts +25 -25
  82. package/src/devtools/worker/redaction-worker.ts +222 -222
  83. package/src/devtools/worker/worker-manager.ts +409 -409
  84. package/src/error/classifier.ts +2 -2
  85. package/src/error/domains.ts +265 -265
  86. package/src/error/formatter.ts +32 -32
  87. package/src/error/result.ts +46 -46
  88. package/src/error/stack-analyzer.ts +5 -0
  89. package/src/error/types.ts +6 -6
  90. package/src/errors/extractor.ts +409 -409
  91. package/src/errors/index.ts +19 -19
  92. package/src/filling/auth.ts +308 -308
  93. package/src/filling/context.ts +569 -569
  94. package/src/filling/deps.ts +238 -238
  95. package/src/generator/contract-glue.ts +2 -1
  96. package/src/generator/generate.ts +12 -10
  97. package/src/generator/index.ts +3 -3
  98. package/src/generator/templates.ts +80 -79
  99. package/src/guard/analyzer.ts +360 -360
  100. package/src/guard/ast-analyzer.ts +806 -806
  101. package/src/guard/auto-correct.ts +1 -1
  102. package/src/guard/check.ts +128 -128
  103. package/src/guard/contract-guard.ts +9 -9
  104. package/src/guard/file-type.test.ts +24 -24
  105. package/src/guard/healing.ts +2 -0
  106. package/src/guard/index.ts +2 -0
  107. package/src/guard/negotiation.ts +430 -4
  108. package/src/guard/presets/atomic.ts +70 -70
  109. package/src/guard/presets/clean.ts +77 -77
  110. package/src/guard/presets/cqrs.test.ts +175 -0
  111. package/src/guard/presets/cqrs.ts +107 -0
  112. package/src/guard/presets/fsd.ts +79 -79
  113. package/src/guard/presets/hexagonal.ts +68 -68
  114. package/src/guard/presets/index.ts +291 -288
  115. package/src/guard/reporter.ts +445 -445
  116. package/src/guard/rules.ts +12 -12
  117. package/src/guard/statistics.ts +578 -578
  118. package/src/guard/suggestions.ts +358 -352
  119. package/src/guard/types.ts +348 -347
  120. package/src/guard/validator.ts +834 -834
  121. package/src/guard/watcher.ts +404 -404
  122. package/src/index.ts +1 -0
  123. package/src/intent/index.ts +310 -310
  124. package/src/island/index.ts +304 -304
  125. package/src/logging/index.ts +22 -22
  126. package/src/logging/transports.ts +365 -365
  127. package/src/paths.test.ts +47 -0
  128. package/src/paths.ts +47 -0
  129. package/src/plugins/index.ts +38 -38
  130. package/src/plugins/registry.ts +377 -377
  131. package/src/plugins/types.ts +363 -363
  132. package/src/report/build.ts +1 -1
  133. package/src/report/index.ts +1 -1
  134. package/src/router/fs-patterns.ts +387 -387
  135. package/src/router/fs-routes.ts +344 -401
  136. package/src/router/fs-scanner.ts +497 -497
  137. package/src/router/fs-types.ts +270 -278
  138. package/src/router/index.ts +81 -81
  139. package/src/runtime/boundary.tsx +232 -232
  140. package/src/runtime/compose.ts +222 -222
  141. package/src/runtime/lifecycle.ts +381 -381
  142. package/src/runtime/logger.test.ts +345 -345
  143. package/src/runtime/logger.ts +677 -677
  144. package/src/runtime/router.test.ts +476 -476
  145. package/src/runtime/router.ts +105 -105
  146. package/src/runtime/security.ts +155 -155
  147. package/src/runtime/server.ts +24 -24
  148. package/src/runtime/session-key.ts +328 -328
  149. package/src/runtime/ssr.ts +367 -367
  150. package/src/runtime/streaming-ssr.ts +1245 -1245
  151. package/src/runtime/trace.ts +144 -144
  152. package/src/seo/index.ts +214 -214
  153. package/src/seo/integration/ssr.ts +307 -307
  154. package/src/seo/render/basic.ts +427 -427
  155. package/src/seo/render/index.ts +143 -143
  156. package/src/seo/render/jsonld.ts +539 -539
  157. package/src/seo/render/opengraph.ts +191 -191
  158. package/src/seo/render/robots.ts +116 -116
  159. package/src/seo/render/sitemap.ts +137 -137
  160. package/src/seo/render/twitter.ts +126 -126
  161. package/src/seo/resolve/index.ts +353 -353
  162. package/src/seo/resolve/opengraph.ts +143 -143
  163. package/src/seo/resolve/robots.ts +73 -73
  164. package/src/seo/resolve/title.ts +94 -94
  165. package/src/seo/resolve/twitter.ts +73 -73
  166. package/src/seo/resolve/url.ts +97 -97
  167. package/src/seo/routes/index.ts +290 -290
  168. package/src/seo/types.ts +575 -575
  169. package/src/slot/validator.ts +39 -39
  170. package/src/spec/index.ts +3 -3
  171. package/src/spec/load.ts +76 -76
  172. package/src/spec/lock.ts +56 -56
  173. package/src/utils/bun.ts +8 -8
  174. package/src/utils/lru-cache.ts +75 -75
  175. package/src/utils/safe-io.ts +188 -188
  176. package/src/utils/string-safe.ts +298 -298
  177. package/src/watcher/rules.ts +5 -5
@@ -1,19 +1,19 @@
1
- /**
2
- * DNA-007: Error Utilities
3
- *
4
- * 에러 코드 추출 및 분류 유틸리티
5
- */
6
-
7
- export {
8
- extractErrorCode,
9
- extractStatusCode,
10
- extractErrorMessage,
11
- extractErrorInfo,
12
- classifyError,
13
- formatUncaughtError,
14
- isErrorCategory,
15
- isRetryableError,
16
- serializeError,
17
- type ErrorCategory,
18
- type ExtractedErrorInfo,
19
- } from "./extractor.js";
1
+ /**
2
+ * DNA-007: Error Utilities
3
+ *
4
+ * 에러 코드 추출 및 분류 유틸리티
5
+ */
6
+
7
+ export {
8
+ extractErrorCode,
9
+ extractStatusCode,
10
+ extractErrorMessage,
11
+ extractErrorInfo,
12
+ classifyError,
13
+ formatUncaughtError,
14
+ isErrorCategory,
15
+ isRetryableError,
16
+ serializeError,
17
+ type ErrorCategory,
18
+ type ExtractedErrorInfo,
19
+ } from "./extractor.js";
@@ -1,308 +1,308 @@
1
- /**
2
- * Mandu Auth Guards - 인증/인가 헬퍼 🔐
3
- *
4
- * beforeHandle에서 사용할 수 있는 타입-안전 인증 헬퍼
5
- * 인증 실패 시 적절한 에러를 throw하여 체인 중단
6
- */
7
-
8
- import type { ManduContext } from "./context";
9
-
10
- /**
11
- * 인증 실패 에러 (401 Unauthorized)
12
- */
13
- export class AuthenticationError extends Error {
14
- readonly statusCode = 401;
15
-
16
- constructor(message: string = "Authentication required") {
17
- super(message);
18
- this.name = "AuthenticationError";
19
- }
20
- }
21
-
22
- /**
23
- * 인가 실패 에러 (403 Forbidden)
24
- */
25
- export class AuthorizationError extends Error {
26
- readonly statusCode = 403;
27
- readonly requiredRoles?: string[];
28
-
29
- constructor(message: string = "Access denied", requiredRoles?: string[]) {
30
- super(message);
31
- this.name = "AuthorizationError";
32
- this.requiredRoles = requiredRoles;
33
- }
34
- }
35
-
36
- /**
37
- * 기본 User 인터페이스
38
- * 프로젝트에서 확장하여 사용
39
- */
40
- export interface BaseUser {
41
- id: string;
42
- [key: string]: unknown;
43
- }
44
-
45
- /**
46
- * Role을 가진 User 인터페이스
47
- */
48
- export interface UserWithRole extends BaseUser {
49
- role: string;
50
- }
51
-
52
- /**
53
- * Roles 배열을 가진 User 인터페이스
54
- */
55
- export interface UserWithRoles extends BaseUser {
56
- roles: string[];
57
- }
58
-
59
- // ============================================
60
- // 🔐 Auth Helpers
61
- // ============================================
62
-
63
- /**
64
- * 인증된 사용자 필수
65
- * beforeHandle에서 user가 없으면 AuthenticationError throw
66
- *
67
- * @param ctx ManduContext
68
- * @param key store에서 user를 찾을 키 (기본: 'user')
69
- * @returns 인증된 User (타입 확정)
70
- * @throws AuthenticationError
71
- *
72
- * @example
73
- * typescript
74
- * import { requireUser } from '@mandujs/core'
75
- *
76
- * export default Mandu.filling()
77
- * .beforeHandle(async (ctx) => {
78
- * // JWT 토큰 검증 후 user 저장
79
- * const user = await verifyToken(ctx.headers.get('Authorization'));
80
- * ctx.set('user', user);
81
- * // void 반환 시 계속 진행
82
- * })
83
- * .get((ctx) => {
84
- * const user = requireUser(ctx); // User 타입 확정, 없으면 401
85
- * return ctx.ok({ message: "Hello, " + user.id + "!" });
86
- * })
87
- *
88
- */
89
- export function requireUser<T extends BaseUser = BaseUser>(
90
- ctx: ManduContext,
91
- key: string = "user"
92
- ): T {
93
- const user = ctx.get<T>(key);
94
-
95
- if (!user) {
96
- throw new AuthenticationError("User context is required");
97
- }
98
-
99
- if (typeof user !== "object" || !("id" in user)) {
100
- throw new AuthenticationError("Invalid user context");
101
- }
102
-
103
- return user;
104
- }
105
-
106
- /**
107
- * 특정 역할 필수 (단일 role 필드)
108
- *
109
- * @param ctx ManduContext
110
- * @param roles 허용된 역할 목록
111
- * @param key store에서 user를 찾을 키 (기본: 'user')
112
- * @returns 인증된 User (타입 확정)
113
- * @throws AuthenticationError (user 없음)
114
- * @throws AuthorizationError (역할 불일치)
115
- *
116
- * @example
117
- * typescript
118
- * .beforeHandle((ctx) => {
119
- * requireRole(ctx, 'admin', 'moderator'); // admin 또는 moderator만 허용
120
- * // void 반환 시 계속 진행
121
- * })
122
- *
123
- */
124
- export function requireRole<T extends UserWithRole = UserWithRole>(
125
- ctx: ManduContext,
126
- ...roles: string[]
127
- ): T {
128
- const user = requireUser<T>(ctx);
129
-
130
- if (!("role" in user) || typeof user.role !== "string") {
131
- throw new AuthorizationError("User has no role defined");
132
- }
133
-
134
- if (!roles.includes(user.role)) {
135
- throw new AuthorizationError(
136
- "Required role: " + roles.join(" or "),
137
- roles
138
- );
139
- }
140
-
141
- return user;
142
- }
143
-
144
- /**
145
- * 특정 역할 중 하나 필수 (roles 배열 필드)
146
- *
147
- * @param ctx ManduContext
148
- * @param roles 허용된 역할 목록 (하나라도 있으면 통과)
149
- * @param key store에서 user를 찾을 키 (기본: 'user')
150
- * @returns 인증된 User (타입 확정)
151
- * @throws AuthenticationError (user 없음)
152
- * @throws AuthorizationError (역할 불일치)
153
- *
154
- * @example
155
- * typescript
156
- * .beforeHandle((ctx) => {
157
- * requireAnyRole(ctx, 'editor', 'admin'); // editor 또는 admin 역할 필요
158
- * // void 반환 시 계속 진행
159
- * })
160
- *
161
- */
162
- export function requireAnyRole<T extends UserWithRoles = UserWithRoles>(
163
- ctx: ManduContext,
164
- ...roles: string[]
165
- ): T {
166
- const user = requireUser<T>(ctx);
167
-
168
- if (!("roles" in user) || !Array.isArray(user.roles)) {
169
- throw new AuthorizationError("User has no roles defined");
170
- }
171
-
172
- const hasRole = roles.some((role) => user.roles.includes(role));
173
-
174
- if (!hasRole) {
175
- throw new AuthorizationError(
176
- "Required one of roles: " + roles.join(", "),
177
- roles
178
- );
179
- }
180
-
181
- return user;
182
- }
183
-
184
- /**
185
- * 모든 역할 필수 (roles 배열 필드)
186
- *
187
- * @param ctx ManduContext
188
- * @param roles 필요한 역할 목록 (모두 있어야 통과)
189
- * @returns 인증된 User (타입 확정)
190
- * @throws AuthenticationError (user 없음)
191
- * @throws AuthorizationError (역할 불일치)
192
- *
193
- * @example
194
- * typescript
195
- * .beforeHandle((ctx) => {
196
- * requireAllRoles(ctx, 'verified', 'premium'); // verified AND premium 필요
197
- * // void 반환 시 계속 진행
198
- * })
199
- *
200
- */
201
- export function requireAllRoles<T extends UserWithRoles = UserWithRoles>(
202
- ctx: ManduContext,
203
- ...roles: string[]
204
- ): T {
205
- const user = requireUser<T>(ctx);
206
-
207
- if (!("roles" in user) || !Array.isArray(user.roles)) {
208
- throw new AuthorizationError("User has no roles defined");
209
- }
210
-
211
- const missingRoles = roles.filter((role) => !user.roles.includes(role));
212
-
213
- if (missingRoles.length > 0) {
214
- throw new AuthorizationError(
215
- "Missing required roles: " + missingRoles.join(", "),
216
- roles
217
- );
218
- }
219
-
220
- return user;
221
- }
222
-
223
- // ============================================
224
- // 🔐 Auth Handler Factory
225
- // ============================================
226
-
227
- /**
228
- * 인증 beforeHandle 생성 팩토리
229
- * 반복되는 인증 로직을 beforeHandle로 변환
230
- *
231
- * @example
232
- * typescript
233
- * const authHandler = createAuthGuard(async (ctx) => {
234
- * const token = ctx.headers.get('Authorization')?.replace('Bearer ', '');
235
- * if (!token) return null;
236
- * return await verifyJwt(token);
237
- * });
238
- *
239
- * export default Mandu.filling()
240
- * .beforeHandle(authHandler)
241
- * .get((ctx) => {
242
- * const user = requireUser(ctx);
243
- * return ctx.ok({ user });
244
- * })
245
- *
246
- */
247
- export function createAuthGuard<T extends BaseUser>(
248
- authenticator: (ctx: ManduContext) => T | null | Promise<T | null>,
249
- options: {
250
- key?: string;
251
- onUnauthenticated?: (ctx: ManduContext) => Response;
252
- } = {}
253
- ) {
254
- const { key = "user", onUnauthenticated } = options;
255
-
256
- return async (ctx: ManduContext): Promise<Response | void> => {
257
- try {
258
- const user = await authenticator(ctx);
259
-
260
- if (user) {
261
- ctx.set(key, user);
262
- return; // void 반환 시 계속 진행
263
- }
264
-
265
- if (onUnauthenticated) {
266
- return onUnauthenticated(ctx);
267
- }
268
-
269
- return ctx.unauthorized("Authentication required");
270
- } catch (error) {
271
- if (error instanceof AuthenticationError) {
272
- return ctx.unauthorized(error.message);
273
- }
274
- throw error;
275
- }
276
- };
277
- }
278
-
279
- /**
280
- * 역할 기반 beforeHandle 생성 팩토리
281
- *
282
- * @example
283
- * typescript
284
- * const adminOnly = createRoleGuard('admin');
285
- * const editorOrAdmin = createRoleGuard('editor', 'admin');
286
- *
287
- * export default Mandu.filling()
288
- * .beforeHandle(authHandler)
289
- * .beforeHandle(adminOnly) // admin만 접근 가능
290
- * .delete((ctx) => ctx.noContent())
291
- *
292
- */
293
- export function createRoleGuard(...allowedRoles: string[]) {
294
- return (ctx: ManduContext): Response | void => {
295
- try {
296
- requireRole(ctx, ...allowedRoles);
297
- return; // void 반환 시 계속 진행
298
- } catch (error) {
299
- if (error instanceof AuthenticationError) {
300
- return ctx.unauthorized(error.message);
301
- }
302
- if (error instanceof AuthorizationError) {
303
- return ctx.forbidden(error.message);
304
- }
305
- throw error;
306
- }
307
- };
308
- }
1
+ /**
2
+ * Mandu Auth Guards - 인증/인가 헬퍼 🔐
3
+ *
4
+ * beforeHandle에서 사용할 수 있는 타입-안전 인증 헬퍼
5
+ * 인증 실패 시 적절한 에러를 throw하여 체인 중단
6
+ */
7
+
8
+ import type { ManduContext } from "./context";
9
+
10
+ /**
11
+ * 인증 실패 에러 (401 Unauthorized)
12
+ */
13
+ export class AuthenticationError extends Error {
14
+ readonly statusCode = 401;
15
+
16
+ constructor(message: string = "Authentication required") {
17
+ super(message);
18
+ this.name = "AuthenticationError";
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 인가 실패 에러 (403 Forbidden)
24
+ */
25
+ export class AuthorizationError extends Error {
26
+ readonly statusCode = 403;
27
+ readonly requiredRoles?: string[];
28
+
29
+ constructor(message: string = "Access denied", requiredRoles?: string[]) {
30
+ super(message);
31
+ this.name = "AuthorizationError";
32
+ this.requiredRoles = requiredRoles;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 기본 User 인터페이스
38
+ * 프로젝트에서 확장하여 사용
39
+ */
40
+ export interface BaseUser {
41
+ id: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /**
46
+ * Role을 가진 User 인터페이스
47
+ */
48
+ export interface UserWithRole extends BaseUser {
49
+ role: string;
50
+ }
51
+
52
+ /**
53
+ * Roles 배열을 가진 User 인터페이스
54
+ */
55
+ export interface UserWithRoles extends BaseUser {
56
+ roles: string[];
57
+ }
58
+
59
+ // ============================================
60
+ // 🔐 Auth Helpers
61
+ // ============================================
62
+
63
+ /**
64
+ * 인증된 사용자 필수
65
+ * beforeHandle에서 user가 없으면 AuthenticationError throw
66
+ *
67
+ * @param ctx ManduContext
68
+ * @param key store에서 user를 찾을 키 (기본: 'user')
69
+ * @returns 인증된 User (타입 확정)
70
+ * @throws AuthenticationError
71
+ *
72
+ * @example
73
+ * typescript
74
+ * import { requireUser } from '@mandujs/core'
75
+ *
76
+ * export default Mandu.filling()
77
+ * .beforeHandle(async (ctx) => {
78
+ * // JWT 토큰 검증 후 user 저장
79
+ * const user = await verifyToken(ctx.headers.get('Authorization'));
80
+ * ctx.set('user', user);
81
+ * // void 반환 시 계속 진행
82
+ * })
83
+ * .get((ctx) => {
84
+ * const user = requireUser(ctx); // User 타입 확정, 없으면 401
85
+ * return ctx.ok({ message: "Hello, " + user.id + "!" });
86
+ * })
87
+ *
88
+ */
89
+ export function requireUser<T extends BaseUser = BaseUser>(
90
+ ctx: ManduContext,
91
+ key: string = "user"
92
+ ): T {
93
+ const user = ctx.get<T>(key);
94
+
95
+ if (!user) {
96
+ throw new AuthenticationError("User context is required");
97
+ }
98
+
99
+ if (typeof user !== "object" || !("id" in user)) {
100
+ throw new AuthenticationError("Invalid user context");
101
+ }
102
+
103
+ return user;
104
+ }
105
+
106
+ /**
107
+ * 특정 역할 필수 (단일 role 필드)
108
+ *
109
+ * @param ctx ManduContext
110
+ * @param roles 허용된 역할 목록
111
+ * @param key store에서 user를 찾을 키 (기본: 'user')
112
+ * @returns 인증된 User (타입 확정)
113
+ * @throws AuthenticationError (user 없음)
114
+ * @throws AuthorizationError (역할 불일치)
115
+ *
116
+ * @example
117
+ * typescript
118
+ * .beforeHandle((ctx) => {
119
+ * requireRole(ctx, 'admin', 'moderator'); // admin 또는 moderator만 허용
120
+ * // void 반환 시 계속 진행
121
+ * })
122
+ *
123
+ */
124
+ export function requireRole<T extends UserWithRole = UserWithRole>(
125
+ ctx: ManduContext,
126
+ ...roles: string[]
127
+ ): T {
128
+ const user = requireUser<T>(ctx);
129
+
130
+ if (!("role" in user) || typeof user.role !== "string") {
131
+ throw new AuthorizationError("User has no role defined");
132
+ }
133
+
134
+ if (!roles.includes(user.role)) {
135
+ throw new AuthorizationError(
136
+ "Required role: " + roles.join(" or "),
137
+ roles
138
+ );
139
+ }
140
+
141
+ return user;
142
+ }
143
+
144
+ /**
145
+ * 특정 역할 중 하나 필수 (roles 배열 필드)
146
+ *
147
+ * @param ctx ManduContext
148
+ * @param roles 허용된 역할 목록 (하나라도 있으면 통과)
149
+ * @param key store에서 user를 찾을 키 (기본: 'user')
150
+ * @returns 인증된 User (타입 확정)
151
+ * @throws AuthenticationError (user 없음)
152
+ * @throws AuthorizationError (역할 불일치)
153
+ *
154
+ * @example
155
+ * typescript
156
+ * .beforeHandle((ctx) => {
157
+ * requireAnyRole(ctx, 'editor', 'admin'); // editor 또는 admin 역할 필요
158
+ * // void 반환 시 계속 진행
159
+ * })
160
+ *
161
+ */
162
+ export function requireAnyRole<T extends UserWithRoles = UserWithRoles>(
163
+ ctx: ManduContext,
164
+ ...roles: string[]
165
+ ): T {
166
+ const user = requireUser<T>(ctx);
167
+
168
+ if (!("roles" in user) || !Array.isArray(user.roles)) {
169
+ throw new AuthorizationError("User has no roles defined");
170
+ }
171
+
172
+ const hasRole = roles.some((role) => user.roles.includes(role));
173
+
174
+ if (!hasRole) {
175
+ throw new AuthorizationError(
176
+ "Required one of roles: " + roles.join(", "),
177
+ roles
178
+ );
179
+ }
180
+
181
+ return user;
182
+ }
183
+
184
+ /**
185
+ * 모든 역할 필수 (roles 배열 필드)
186
+ *
187
+ * @param ctx ManduContext
188
+ * @param roles 필요한 역할 목록 (모두 있어야 통과)
189
+ * @returns 인증된 User (타입 확정)
190
+ * @throws AuthenticationError (user 없음)
191
+ * @throws AuthorizationError (역할 불일치)
192
+ *
193
+ * @example
194
+ * typescript
195
+ * .beforeHandle((ctx) => {
196
+ * requireAllRoles(ctx, 'verified', 'premium'); // verified AND premium 필요
197
+ * // void 반환 시 계속 진행
198
+ * })
199
+ *
200
+ */
201
+ export function requireAllRoles<T extends UserWithRoles = UserWithRoles>(
202
+ ctx: ManduContext,
203
+ ...roles: string[]
204
+ ): T {
205
+ const user = requireUser<T>(ctx);
206
+
207
+ if (!("roles" in user) || !Array.isArray(user.roles)) {
208
+ throw new AuthorizationError("User has no roles defined");
209
+ }
210
+
211
+ const missingRoles = roles.filter((role) => !user.roles.includes(role));
212
+
213
+ if (missingRoles.length > 0) {
214
+ throw new AuthorizationError(
215
+ "Missing required roles: " + missingRoles.join(", "),
216
+ roles
217
+ );
218
+ }
219
+
220
+ return user;
221
+ }
222
+
223
+ // ============================================
224
+ // 🔐 Auth Handler Factory
225
+ // ============================================
226
+
227
+ /**
228
+ * 인증 beforeHandle 생성 팩토리
229
+ * 반복되는 인증 로직을 beforeHandle로 변환
230
+ *
231
+ * @example
232
+ * typescript
233
+ * const authHandler = createAuthGuard(async (ctx) => {
234
+ * const token = ctx.headers.get('Authorization')?.replace('Bearer ', '');
235
+ * if (!token) return null;
236
+ * return await verifyJwt(token);
237
+ * });
238
+ *
239
+ * export default Mandu.filling()
240
+ * .beforeHandle(authHandler)
241
+ * .get((ctx) => {
242
+ * const user = requireUser(ctx);
243
+ * return ctx.ok({ user });
244
+ * })
245
+ *
246
+ */
247
+ export function createAuthGuard<T extends BaseUser>(
248
+ authenticator: (ctx: ManduContext) => T | null | Promise<T | null>,
249
+ options: {
250
+ key?: string;
251
+ onUnauthenticated?: (ctx: ManduContext) => Response;
252
+ } = {}
253
+ ) {
254
+ const { key = "user", onUnauthenticated } = options;
255
+
256
+ return async (ctx: ManduContext): Promise<Response | void> => {
257
+ try {
258
+ const user = await authenticator(ctx);
259
+
260
+ if (user) {
261
+ ctx.set(key, user);
262
+ return; // void 반환 시 계속 진행
263
+ }
264
+
265
+ if (onUnauthenticated) {
266
+ return onUnauthenticated(ctx);
267
+ }
268
+
269
+ return ctx.unauthorized("Authentication required");
270
+ } catch (error) {
271
+ if (error instanceof AuthenticationError) {
272
+ return ctx.unauthorized(error.message);
273
+ }
274
+ throw error;
275
+ }
276
+ };
277
+ }
278
+
279
+ /**
280
+ * 역할 기반 beforeHandle 생성 팩토리
281
+ *
282
+ * @example
283
+ * typescript
284
+ * const adminOnly = createRoleGuard('admin');
285
+ * const editorOrAdmin = createRoleGuard('editor', 'admin');
286
+ *
287
+ * export default Mandu.filling()
288
+ * .beforeHandle(authHandler)
289
+ * .beforeHandle(adminOnly) // admin만 접근 가능
290
+ * .delete((ctx) => ctx.noContent())
291
+ *
292
+ */
293
+ export function createRoleGuard(...allowedRoles: string[]) {
294
+ return (ctx: ManduContext): Response | void => {
295
+ try {
296
+ requireRole(ctx, ...allowedRoles);
297
+ return; // void 반환 시 계속 진행
298
+ } catch (error) {
299
+ if (error instanceof AuthenticationError) {
300
+ return ctx.unauthorized(error.message);
301
+ }
302
+ if (error instanceof AuthorizationError) {
303
+ return ctx.forbidden(error.message);
304
+ }
305
+ throw error;
306
+ }
307
+ };
308
+ }