@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,328 +1,328 @@
1
- /**
2
- * DNA-004: Session Key Utilities
3
- *
4
- * 세션 키 기반 격리 시스템
5
- * - SSR 상태 격리 (팀별, 사용자별)
6
- * - 캐시 키 생성 (route + params + user)
7
- * - WebSocket 채널 격리 (향후)
8
- */
9
-
10
- /**
11
- * 세션 스코프
12
- */
13
- export type SessionScope =
14
- | "global" // 전역 (모든 사용자 공유)
15
- | "team" // 팀별 격리
16
- | "user" // 사용자별 격리
17
- | "request"; // 요청별 격리 (캐시 없음)
18
-
19
- /**
20
- * 세션 키 빌더 옵션
21
- */
22
- export interface SessionKeyOptions {
23
- /** 라우트 ID 또는 경로 */
24
- route: string;
25
-
26
- /** 팀 ID (team/user 스코프에서 사용) */
27
- teamId?: string;
28
-
29
- /** 사용자 ID (user 스코프에서 사용) */
30
- userId?: string;
31
-
32
- /** 세션 스코프 */
33
- scope: SessionScope;
34
-
35
- /** 추가 파라미터 (쿼리 파라미터 등) */
36
- params?: Record<string, string>;
37
-
38
- /** 네임스페이스 (기본: "session") */
39
- namespace?: string;
40
- }
41
-
42
- /**
43
- * 캐시 키 빌더 옵션
44
- */
45
- export interface CacheKeyOptions {
46
- /** 캐시 타입 (예: "ssr", "api", "data") */
47
- type: string;
48
-
49
- /** 리소스 식별자 */
50
- resource: string;
51
-
52
- /** 버전 또는 태그 */
53
- version?: string;
54
-
55
- /** 추가 파라미터 */
56
- params?: Record<string, string | number | boolean>;
57
-
58
- /** 사용자 ID (사용자별 캐시 시) */
59
- userId?: string;
60
- }
61
-
62
- /**
63
- * 문자열 정규화 (소문자, 특수문자 제거)
64
- */
65
- function normalize(str: string): string {
66
- return str
67
- .toLowerCase()
68
- .replace(/[^a-z0-9_-]/g, "-")
69
- .replace(/-+/g, "-")
70
- .replace(/^-|-$/g, "");
71
- }
72
-
73
- /**
74
- * 파라미터를 정렬된 문자열로 변환
75
- */
76
- function serializeParams(params: Record<string, string | number | boolean>): string {
77
- const entries = Object.entries(params)
78
- .filter(([_, v]) => v !== undefined && v !== null && v !== "")
79
- .sort(([a], [b]) => a.localeCompare(b));
80
-
81
- if (entries.length === 0) return "";
82
-
83
- return entries
84
- .map(([k, v]) => `${normalize(k)}=${normalize(String(v))}`)
85
- .join("&");
86
- }
87
-
88
- /**
89
- * 세션 키 생성
90
- *
91
- * @example
92
- * ```ts
93
- * // 전역 세션
94
- * buildSessionKey({ route: "/dashboard", scope: "global" })
95
- * // → "session:/dashboard"
96
- *
97
- * // 팀별 세션
98
- * buildSessionKey({ route: "/projects", scope: "team", teamId: "team-123" })
99
- * // → "session:/projects:team:team-123"
100
- *
101
- * // 사용자별 세션
102
- * buildSessionKey({ route: "/profile", scope: "user", userId: "user-456" })
103
- * // → "session:/profile:user:user-456"
104
- *
105
- * // 파라미터 포함
106
- * buildSessionKey({
107
- * route: "/search",
108
- * scope: "user",
109
- * userId: "user-456",
110
- * params: { q: "mandu", page: "1" }
111
- * })
112
- * // → "session:/search:user:user-456:page=1&q=mandu"
113
- * ```
114
- */
115
- export function buildSessionKey(options: SessionKeyOptions): string {
116
- const { route, teamId, userId, scope, params, namespace = "session" } = options;
117
- const parts: string[] = [namespace, normalize(route)];
118
-
119
- switch (scope) {
120
- case "team":
121
- if (teamId) {
122
- parts.push(`team:${normalize(teamId)}`);
123
- }
124
- break;
125
-
126
- case "user":
127
- if (userId) {
128
- parts.push(`user:${normalize(userId)}`);
129
- } else if (teamId) {
130
- // user 스코프에서 teamId만 있으면 팀 레벨로 폴백
131
- parts.push(`team:${normalize(teamId)}`);
132
- }
133
- break;
134
-
135
- case "request":
136
- // 요청별 고유 키 (타임스탬프 + 랜덤)
137
- parts.push(`req:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
138
- break;
139
-
140
- case "global":
141
- default:
142
- // 추가 스코프 없음
143
- break;
144
- }
145
-
146
- // 파라미터 추가
147
- if (params && Object.keys(params).length > 0) {
148
- const serialized = serializeParams(params);
149
- if (serialized) {
150
- parts.push(serialized);
151
- }
152
- }
153
-
154
- return parts.join(":");
155
- }
156
-
157
- /**
158
- * 캐시 키 생성
159
- *
160
- * @example
161
- * ```ts
162
- * // SSR 캐시
163
- * buildCacheKey({ type: "ssr", resource: "/blog/123" })
164
- * // → "cache:ssr:/blog/123"
165
- *
166
- * // 버전 포함
167
- * buildCacheKey({ type: "api", resource: "users", version: "v2" })
168
- * // → "cache:api:users:v2"
169
- *
170
- * // 파라미터 포함
171
- * buildCacheKey({
172
- * type: "data",
173
- * resource: "products",
174
- * params: { category: "food", limit: 10 }
175
- * })
176
- * // → "cache:data:products:category=food&limit=10"
177
- *
178
- * // 사용자별 캐시
179
- * buildCacheKey({
180
- * type: "ssr",
181
- * resource: "/dashboard",
182
- * userId: "user-123"
183
- * })
184
- * // → "cache:ssr:/dashboard:user:user-123"
185
- * ```
186
- */
187
- export function buildCacheKey(options: CacheKeyOptions): string {
188
- const { type, resource, version, params, userId } = options;
189
- const parts: string[] = ["cache", normalize(type), normalize(resource)];
190
-
191
- if (version) {
192
- parts.push(normalize(version));
193
- }
194
-
195
- if (userId) {
196
- parts.push(`user:${normalize(userId)}`);
197
- }
198
-
199
- if (params && Object.keys(params).length > 0) {
200
- const serialized = serializeParams(params);
201
- if (serialized) {
202
- parts.push(serialized);
203
- }
204
- }
205
-
206
- return parts.join(":");
207
- }
208
-
209
- /**
210
- * WebSocket 채널 키 생성
211
- *
212
- * @example
213
- * ```ts
214
- * buildChannelKey({ channel: "notifications", userId: "user-123" })
215
- * // → "ws:notifications:user:user-123"
216
- *
217
- * buildChannelKey({ channel: "team-chat", teamId: "team-456" })
218
- * // → "ws:team-chat:team:team-456"
219
- * ```
220
- */
221
- export function buildChannelKey(options: {
222
- channel: string;
223
- userId?: string;
224
- teamId?: string;
225
- }): string {
226
- const { channel, userId, teamId } = options;
227
- const parts: string[] = ["ws", normalize(channel)];
228
-
229
- if (userId) {
230
- parts.push(`user:${normalize(userId)}`);
231
- } else if (teamId) {
232
- parts.push(`team:${normalize(teamId)}`);
233
- }
234
-
235
- return parts.join(":");
236
- }
237
-
238
- /**
239
- * 세션 키 파싱
240
- *
241
- * @example
242
- * ```ts
243
- * parseSessionKey("session:/dashboard:team:team-123")
244
- * // → { namespace: "session", route: "/dashboard", scope: "team", teamId: "team-123" }
245
- * ```
246
- */
247
- export function parseSessionKey(key: string): {
248
- namespace: string;
249
- route: string;
250
- scope: SessionScope;
251
- teamId?: string;
252
- userId?: string;
253
- params?: string;
254
- } | null {
255
- const parts = key.split(":");
256
-
257
- if (parts.length < 2) return null;
258
-
259
- const [namespace, route, ...rest] = parts;
260
- let scope: SessionScope = "global";
261
- let teamId: string | undefined;
262
- let userId: string | undefined;
263
- let params: string | undefined;
264
-
265
- for (let i = 0; i < rest.length; i++) {
266
- const part = rest[i];
267
-
268
- if (part === "team" && rest[i + 1]) {
269
- scope = "team";
270
- teamId = rest[++i];
271
- } else if (part === "user" && rest[i + 1]) {
272
- scope = "user";
273
- userId = rest[++i];
274
- } else if (part.startsWith("req:")) {
275
- scope = "request";
276
- } else if (part.includes("=")) {
277
- params = part;
278
- }
279
- }
280
-
281
- return { namespace, route, scope, teamId, userId, params };
282
- }
283
-
284
- /**
285
- * 키 패턴 매칭 (와일드카드 지원)
286
- *
287
- * @example
288
- * ```ts
289
- * matchKeyPattern("session:/dashboard:*", "session:/dashboard:team:team-123")
290
- * // → true
291
- *
292
- * matchKeyPattern("cache:ssr:*", "cache:api:users")
293
- * // → false
294
- * ```
295
- */
296
- export function matchKeyPattern(pattern: string, key: string): boolean {
297
- const patternParts = pattern.split(":");
298
- const keyParts = key.split(":");
299
-
300
- for (let i = 0; i < patternParts.length; i++) {
301
- const p = patternParts[i];
302
-
303
- if (p === "*") {
304
- // 와일드카드는 나머지 모두 매칭
305
- return true;
306
- }
307
-
308
- if (p === "**") {
309
- // 더블 와일드카드는 0개 이상 매칭
310
- if (i === patternParts.length - 1) return true;
311
-
312
- // 나머지 패턴과 매칭되는 위치 찾기
313
- const remaining = patternParts.slice(i + 1);
314
- for (let j = i; j <= keyParts.length - remaining.length; j++) {
315
- if (matchKeyPattern(remaining.join(":"), keyParts.slice(j).join(":"))) {
316
- return true;
317
- }
318
- }
319
- return false;
320
- }
321
-
322
- if (keyParts[i] !== p) {
323
- return false;
324
- }
325
- }
326
-
327
- return patternParts.length === keyParts.length;
328
- }
1
+ /**
2
+ * DNA-004: Session Key Utilities
3
+ *
4
+ * 세션 키 기반 격리 시스템
5
+ * - SSR 상태 격리 (팀별, 사용자별)
6
+ * - 캐시 키 생성 (route + params + user)
7
+ * - WebSocket 채널 격리 (향후)
8
+ */
9
+
10
+ /**
11
+ * 세션 스코프
12
+ */
13
+ export type SessionScope =
14
+ | "global" // 전역 (모든 사용자 공유)
15
+ | "team" // 팀별 격리
16
+ | "user" // 사용자별 격리
17
+ | "request"; // 요청별 격리 (캐시 없음)
18
+
19
+ /**
20
+ * 세션 키 빌더 옵션
21
+ */
22
+ export interface SessionKeyOptions {
23
+ /** 라우트 ID 또는 경로 */
24
+ route: string;
25
+
26
+ /** 팀 ID (team/user 스코프에서 사용) */
27
+ teamId?: string;
28
+
29
+ /** 사용자 ID (user 스코프에서 사용) */
30
+ userId?: string;
31
+
32
+ /** 세션 스코프 */
33
+ scope: SessionScope;
34
+
35
+ /** 추가 파라미터 (쿼리 파라미터 등) */
36
+ params?: Record<string, string>;
37
+
38
+ /** 네임스페이스 (기본: "session") */
39
+ namespace?: string;
40
+ }
41
+
42
+ /**
43
+ * 캐시 키 빌더 옵션
44
+ */
45
+ export interface CacheKeyOptions {
46
+ /** 캐시 타입 (예: "ssr", "api", "data") */
47
+ type: string;
48
+
49
+ /** 리소스 식별자 */
50
+ resource: string;
51
+
52
+ /** 버전 또는 태그 */
53
+ version?: string;
54
+
55
+ /** 추가 파라미터 */
56
+ params?: Record<string, string | number | boolean>;
57
+
58
+ /** 사용자 ID (사용자별 캐시 시) */
59
+ userId?: string;
60
+ }
61
+
62
+ /**
63
+ * 문자열 정규화 (소문자, 특수문자 제거)
64
+ */
65
+ function normalize(str: string): string {
66
+ return str
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9_-]/g, "-")
69
+ .replace(/-+/g, "-")
70
+ .replace(/^-|-$/g, "");
71
+ }
72
+
73
+ /**
74
+ * 파라미터를 정렬된 문자열로 변환
75
+ */
76
+ function serializeParams(params: Record<string, string | number | boolean>): string {
77
+ const entries = Object.entries(params)
78
+ .filter(([_, v]) => v !== undefined && v !== null && v !== "")
79
+ .sort(([a], [b]) => a.localeCompare(b));
80
+
81
+ if (entries.length === 0) return "";
82
+
83
+ return entries
84
+ .map(([k, v]) => `${normalize(k)}=${normalize(String(v))}`)
85
+ .join("&");
86
+ }
87
+
88
+ /**
89
+ * 세션 키 생성
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // 전역 세션
94
+ * buildSessionKey({ route: "/dashboard", scope: "global" })
95
+ * // → "session:/dashboard"
96
+ *
97
+ * // 팀별 세션
98
+ * buildSessionKey({ route: "/projects", scope: "team", teamId: "team-123" })
99
+ * // → "session:/projects:team:team-123"
100
+ *
101
+ * // 사용자별 세션
102
+ * buildSessionKey({ route: "/profile", scope: "user", userId: "user-456" })
103
+ * // → "session:/profile:user:user-456"
104
+ *
105
+ * // 파라미터 포함
106
+ * buildSessionKey({
107
+ * route: "/search",
108
+ * scope: "user",
109
+ * userId: "user-456",
110
+ * params: { q: "mandu", page: "1" }
111
+ * })
112
+ * // → "session:/search:user:user-456:page=1&q=mandu"
113
+ * ```
114
+ */
115
+ export function buildSessionKey(options: SessionKeyOptions): string {
116
+ const { route, teamId, userId, scope, params, namespace = "session" } = options;
117
+ const parts: string[] = [namespace, normalize(route)];
118
+
119
+ switch (scope) {
120
+ case "team":
121
+ if (teamId) {
122
+ parts.push(`team:${normalize(teamId)}`);
123
+ }
124
+ break;
125
+
126
+ case "user":
127
+ if (userId) {
128
+ parts.push(`user:${normalize(userId)}`);
129
+ } else if (teamId) {
130
+ // user 스코프에서 teamId만 있으면 팀 레벨로 폴백
131
+ parts.push(`team:${normalize(teamId)}`);
132
+ }
133
+ break;
134
+
135
+ case "request":
136
+ // 요청별 고유 키 (타임스탬프 + 랜덤)
137
+ parts.push(`req:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
138
+ break;
139
+
140
+ case "global":
141
+ default:
142
+ // 추가 스코프 없음
143
+ break;
144
+ }
145
+
146
+ // 파라미터 추가
147
+ if (params && Object.keys(params).length > 0) {
148
+ const serialized = serializeParams(params);
149
+ if (serialized) {
150
+ parts.push(serialized);
151
+ }
152
+ }
153
+
154
+ return parts.join(":");
155
+ }
156
+
157
+ /**
158
+ * 캐시 키 생성
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * // SSR 캐시
163
+ * buildCacheKey({ type: "ssr", resource: "/blog/123" })
164
+ * // → "cache:ssr:/blog/123"
165
+ *
166
+ * // 버전 포함
167
+ * buildCacheKey({ type: "api", resource: "users", version: "v2" })
168
+ * // → "cache:api:users:v2"
169
+ *
170
+ * // 파라미터 포함
171
+ * buildCacheKey({
172
+ * type: "data",
173
+ * resource: "products",
174
+ * params: { category: "food", limit: 10 }
175
+ * })
176
+ * // → "cache:data:products:category=food&limit=10"
177
+ *
178
+ * // 사용자별 캐시
179
+ * buildCacheKey({
180
+ * type: "ssr",
181
+ * resource: "/dashboard",
182
+ * userId: "user-123"
183
+ * })
184
+ * // → "cache:ssr:/dashboard:user:user-123"
185
+ * ```
186
+ */
187
+ export function buildCacheKey(options: CacheKeyOptions): string {
188
+ const { type, resource, version, params, userId } = options;
189
+ const parts: string[] = ["cache", normalize(type), normalize(resource)];
190
+
191
+ if (version) {
192
+ parts.push(normalize(version));
193
+ }
194
+
195
+ if (userId) {
196
+ parts.push(`user:${normalize(userId)}`);
197
+ }
198
+
199
+ if (params && Object.keys(params).length > 0) {
200
+ const serialized = serializeParams(params);
201
+ if (serialized) {
202
+ parts.push(serialized);
203
+ }
204
+ }
205
+
206
+ return parts.join(":");
207
+ }
208
+
209
+ /**
210
+ * WebSocket 채널 키 생성
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * buildChannelKey({ channel: "notifications", userId: "user-123" })
215
+ * // → "ws:notifications:user:user-123"
216
+ *
217
+ * buildChannelKey({ channel: "team-chat", teamId: "team-456" })
218
+ * // → "ws:team-chat:team:team-456"
219
+ * ```
220
+ */
221
+ export function buildChannelKey(options: {
222
+ channel: string;
223
+ userId?: string;
224
+ teamId?: string;
225
+ }): string {
226
+ const { channel, userId, teamId } = options;
227
+ const parts: string[] = ["ws", normalize(channel)];
228
+
229
+ if (userId) {
230
+ parts.push(`user:${normalize(userId)}`);
231
+ } else if (teamId) {
232
+ parts.push(`team:${normalize(teamId)}`);
233
+ }
234
+
235
+ return parts.join(":");
236
+ }
237
+
238
+ /**
239
+ * 세션 키 파싱
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * parseSessionKey("session:/dashboard:team:team-123")
244
+ * // → { namespace: "session", route: "/dashboard", scope: "team", teamId: "team-123" }
245
+ * ```
246
+ */
247
+ export function parseSessionKey(key: string): {
248
+ namespace: string;
249
+ route: string;
250
+ scope: SessionScope;
251
+ teamId?: string;
252
+ userId?: string;
253
+ params?: string;
254
+ } | null {
255
+ const parts = key.split(":");
256
+
257
+ if (parts.length < 2) return null;
258
+
259
+ const [namespace, route, ...rest] = parts;
260
+ let scope: SessionScope = "global";
261
+ let teamId: string | undefined;
262
+ let userId: string | undefined;
263
+ let params: string | undefined;
264
+
265
+ for (let i = 0; i < rest.length; i++) {
266
+ const part = rest[i];
267
+
268
+ if (part === "team" && rest[i + 1]) {
269
+ scope = "team";
270
+ teamId = rest[++i];
271
+ } else if (part === "user" && rest[i + 1]) {
272
+ scope = "user";
273
+ userId = rest[++i];
274
+ } else if (part.startsWith("req:")) {
275
+ scope = "request";
276
+ } else if (part.includes("=")) {
277
+ params = part;
278
+ }
279
+ }
280
+
281
+ return { namespace, route, scope, teamId, userId, params };
282
+ }
283
+
284
+ /**
285
+ * 키 패턴 매칭 (와일드카드 지원)
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * matchKeyPattern("session:/dashboard:*", "session:/dashboard:team:team-123")
290
+ * // → true
291
+ *
292
+ * matchKeyPattern("cache:ssr:*", "cache:api:users")
293
+ * // → false
294
+ * ```
295
+ */
296
+ export function matchKeyPattern(pattern: string, key: string): boolean {
297
+ const patternParts = pattern.split(":");
298
+ const keyParts = key.split(":");
299
+
300
+ for (let i = 0; i < patternParts.length; i++) {
301
+ const p = patternParts[i];
302
+
303
+ if (p === "*") {
304
+ // 와일드카드는 나머지 모두 매칭
305
+ return true;
306
+ }
307
+
308
+ if (p === "**") {
309
+ // 더블 와일드카드는 0개 이상 매칭
310
+ if (i === patternParts.length - 1) return true;
311
+
312
+ // 나머지 패턴과 매칭되는 위치 찾기
313
+ const remaining = patternParts.slice(i + 1);
314
+ for (let j = i; j <= keyParts.length - remaining.length; j++) {
315
+ if (matchKeyPattern(remaining.join(":"), keyParts.slice(j).join(":"))) {
316
+ return true;
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
322
+ if (keyParts[i] !== p) {
323
+ return false;
324
+ }
325
+ }
326
+
327
+ return patternParts.length === keyParts.length;
328
+ }