@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.
- package/README.ko.md +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- 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 +9 -0
- package/src/config/validate.ts +12 -0
- 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/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- 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 +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- 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 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- 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/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- 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 +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- 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/errors/index.ts
CHANGED
|
@@ -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";
|
package/src/filling/auth.ts
CHANGED
|
@@ -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
|
+
}
|