@mandujs/core 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.ko.md +4 -4
  2. package/README.md +653 -653
  3. package/package.json +1 -1
  4. package/src/bundler/build.ts +91 -91
  5. package/src/bundler/css.ts +302 -302
  6. package/src/client/Link.tsx +227 -227
  7. package/src/client/globals.ts +44 -44
  8. package/src/client/hooks.ts +267 -267
  9. package/src/client/index.ts +5 -5
  10. package/src/client/island.ts +8 -8
  11. package/src/client/router.ts +435 -435
  12. package/src/client/runtime.ts +23 -23
  13. package/src/client/serialize.ts +404 -404
  14. package/src/client/window-state.ts +101 -101
  15. package/src/config/mandu.ts +9 -0
  16. package/src/config/validate.ts +12 -0
  17. package/src/config/watcher.ts +311 -311
  18. package/src/constants.ts +40 -40
  19. package/src/content/content-layer.ts +314 -314
  20. package/src/content/content.test.ts +433 -433
  21. package/src/content/data-store.ts +245 -245
  22. package/src/content/digest.ts +133 -133
  23. package/src/content/index.ts +164 -164
  24. package/src/content/loader-context.ts +172 -172
  25. package/src/content/loaders/api.ts +216 -216
  26. package/src/content/loaders/file.ts +169 -169
  27. package/src/content/loaders/glob.ts +252 -252
  28. package/src/content/loaders/index.ts +34 -34
  29. package/src/content/loaders/types.ts +137 -137
  30. package/src/content/meta-store.ts +209 -209
  31. package/src/content/types.ts +282 -282
  32. package/src/content/watcher.ts +135 -135
  33. package/src/contract/client-safe.test.ts +42 -42
  34. package/src/contract/client-safe.ts +114 -114
  35. package/src/contract/client.ts +16 -16
  36. package/src/contract/define.ts +459 -459
  37. package/src/contract/handler.ts +10 -10
  38. package/src/contract/normalize.test.ts +276 -276
  39. package/src/contract/normalize.ts +404 -404
  40. package/src/contract/registry.test.ts +206 -206
  41. package/src/contract/registry.ts +568 -568
  42. package/src/contract/schema.ts +48 -48
  43. package/src/contract/types.ts +58 -58
  44. package/src/contract/validator.ts +32 -32
  45. package/src/devtools/ai/context-builder.ts +375 -375
  46. package/src/devtools/ai/index.ts +25 -25
  47. package/src/devtools/ai/mcp-connector.ts +465 -465
  48. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  49. package/src/devtools/client/catchers/index.ts +18 -18
  50. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  51. package/src/devtools/client/components/index.ts +39 -39
  52. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  53. package/src/devtools/client/components/mandu-character.tsx +241 -241
  54. package/src/devtools/client/components/overlay.tsx +368 -368
  55. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  56. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  57. package/src/devtools/client/components/panel/index.ts +32 -32
  58. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  59. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  60. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  61. package/src/devtools/client/filters/context-filters.ts +282 -282
  62. package/src/devtools/client/filters/index.ts +16 -16
  63. package/src/devtools/client/index.ts +63 -63
  64. package/src/devtools/client/persistence.ts +335 -335
  65. package/src/devtools/client/state-manager.ts +478 -478
  66. package/src/devtools/design-tokens.ts +263 -263
  67. package/src/devtools/hook/create-hook.ts +207 -207
  68. package/src/devtools/hook/index.ts +13 -13
  69. package/src/devtools/index.ts +439 -439
  70. package/src/devtools/init.ts +266 -266
  71. package/src/devtools/protocol.ts +237 -237
  72. package/src/devtools/server/index.ts +17 -17
  73. package/src/devtools/server/source-context.ts +444 -444
  74. package/src/devtools/types.ts +319 -319
  75. package/src/devtools/worker/index.ts +25 -25
  76. package/src/devtools/worker/redaction-worker.ts +222 -222
  77. package/src/devtools/worker/worker-manager.ts +409 -409
  78. package/src/error/domains.ts +265 -265
  79. package/src/error/result.ts +46 -46
  80. package/src/error/types.ts +6 -6
  81. package/src/errors/extractor.ts +409 -409
  82. package/src/errors/index.ts +19 -19
  83. package/src/filling/auth.ts +308 -308
  84. package/src/filling/context.ts +24 -1
  85. package/src/filling/deps.ts +238 -238
  86. package/src/filling/index.ts +4 -0
  87. package/src/filling/sse-catchup.test.ts +56 -0
  88. package/src/filling/sse-catchup.ts +67 -0
  89. package/src/filling/sse.test.ts +168 -0
  90. package/src/filling/sse.ts +162 -0
  91. package/src/generator/index.ts +3 -3
  92. package/src/guard/analyzer.ts +360 -360
  93. package/src/guard/ast-analyzer.ts +806 -806
  94. package/src/guard/contract-guard.ts +9 -9
  95. package/src/guard/file-type.test.ts +24 -24
  96. package/src/guard/presets/atomic.ts +70 -70
  97. package/src/guard/presets/clean.ts +77 -77
  98. package/src/guard/presets/fsd.ts +79 -79
  99. package/src/guard/presets/hexagonal.ts +68 -68
  100. package/src/guard/presets/index.ts +291 -291
  101. package/src/guard/reporter.ts +445 -445
  102. package/src/guard/rules.ts +12 -12
  103. package/src/guard/statistics.ts +578 -578
  104. package/src/guard/suggestions.ts +358 -358
  105. package/src/guard/types.ts +348 -348
  106. package/src/guard/validator.ts +834 -834
  107. package/src/guard/watcher.ts +404 -404
  108. package/src/index.ts +6 -1
  109. package/src/intent/index.ts +310 -310
  110. package/src/island/index.ts +304 -304
  111. package/src/logging/index.ts +22 -22
  112. package/src/logging/transports.ts +365 -365
  113. package/src/plugins/index.ts +38 -38
  114. package/src/plugins/registry.ts +377 -377
  115. package/src/plugins/types.ts +363 -363
  116. package/src/report/index.ts +1 -1
  117. package/src/router/fs-patterns.ts +387 -387
  118. package/src/router/fs-scanner.ts +497 -497
  119. package/src/runtime/boundary.tsx +232 -232
  120. package/src/runtime/compose.ts +222 -222
  121. package/src/runtime/escape.ts +44 -0
  122. package/src/runtime/lifecycle.ts +381 -381
  123. package/src/runtime/logger.test.ts +345 -345
  124. package/src/runtime/logger.ts +677 -677
  125. package/src/runtime/router.test.ts +476 -476
  126. package/src/runtime/router.ts +105 -105
  127. package/src/runtime/security.ts +155 -155
  128. package/src/runtime/server.ts +257 -0
  129. package/src/runtime/session-key.ts +328 -328
  130. package/src/runtime/ssr.ts +16 -21
  131. package/src/runtime/streaming-ssr.ts +24 -33
  132. package/src/runtime/trace.ts +144 -144
  133. package/src/seo/index.ts +214 -214
  134. package/src/seo/integration/ssr.ts +307 -307
  135. package/src/seo/render/basic.ts +427 -427
  136. package/src/seo/render/index.ts +143 -143
  137. package/src/seo/render/jsonld.ts +539 -539
  138. package/src/seo/render/opengraph.ts +191 -191
  139. package/src/seo/render/robots.ts +116 -116
  140. package/src/seo/render/sitemap.ts +137 -137
  141. package/src/seo/render/twitter.ts +126 -126
  142. package/src/seo/resolve/index.ts +353 -353
  143. package/src/seo/resolve/opengraph.ts +143 -143
  144. package/src/seo/resolve/robots.ts +73 -73
  145. package/src/seo/resolve/title.ts +94 -94
  146. package/src/seo/resolve/twitter.ts +73 -73
  147. package/src/seo/resolve/url.ts +97 -97
  148. package/src/seo/routes/index.ts +290 -290
  149. package/src/seo/types.ts +575 -575
  150. package/src/slot/validator.ts +39 -39
  151. package/src/spec/index.ts +3 -3
  152. package/src/spec/load.ts +76 -76
  153. package/src/spec/lock.ts +56 -56
  154. package/src/utils/bun.ts +8 -8
  155. package/src/utils/lru-cache.ts +75 -75
  156. package/src/utils/safe-io.ts +188 -188
  157. package/src/utils/string-safe.ts +298 -298
@@ -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
+ }