clawfire 0.1.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 (87) hide show
  1. package/README.md +182 -0
  2. package/dist/admin.cjs +309 -0
  3. package/dist/admin.cjs.map +1 -0
  4. package/dist/admin.d.cts +93 -0
  5. package/dist/admin.d.ts +93 -0
  6. package/dist/admin.js +274 -0
  7. package/dist/admin.js.map +1 -0
  8. package/dist/auth-DQ3cifhb.d.cts +55 -0
  9. package/dist/auth-DtnUPbXT.d.ts +55 -0
  10. package/dist/chunk-37Y2XI7X.js +75 -0
  11. package/dist/chunk-YGIPORYL.js +339 -0
  12. package/dist/cli.js +241 -0
  13. package/dist/client.cjs +97 -0
  14. package/dist/client.cjs.map +1 -0
  15. package/dist/client.d.cts +4 -0
  16. package/dist/client.d.ts +4 -0
  17. package/dist/client.js +68 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/codegen.cjs +648 -0
  20. package/dist/codegen.cjs.map +1 -0
  21. package/dist/codegen.d.cts +25 -0
  22. package/dist/codegen.d.ts +25 -0
  23. package/dist/codegen.js +617 -0
  24. package/dist/codegen.js.map +1 -0
  25. package/dist/config-QMBJRn9G.d.cts +46 -0
  26. package/dist/config-QMBJRn9G.d.ts +46 -0
  27. package/dist/dev-server-QAVWINAT.js +973 -0
  28. package/dist/dev.cjs +1388 -0
  29. package/dist/dev.cjs.map +1 -0
  30. package/dist/dev.d.cts +111 -0
  31. package/dist/dev.d.ts +111 -0
  32. package/dist/dev.js +1349 -0
  33. package/dist/dev.js.map +1 -0
  34. package/dist/discover-BPMAZFBD.js +9 -0
  35. package/dist/discover-DYNqz_ym.d.cts +28 -0
  36. package/dist/discover-DYNqz_ym.d.ts +28 -0
  37. package/dist/errors-s_mP7rs9.d.cts +33 -0
  38. package/dist/errors-s_mP7rs9.d.ts +33 -0
  39. package/dist/functions.cjs +1156 -0
  40. package/dist/functions.cjs.map +1 -0
  41. package/dist/functions.d.cts +115 -0
  42. package/dist/functions.d.ts +115 -0
  43. package/dist/functions.js +1108 -0
  44. package/dist/functions.js.map +1 -0
  45. package/dist/hosting-7WVFHAYJ.js +85 -0
  46. package/dist/html-PCUCJGBH.js +7 -0
  47. package/dist/index.cjs +349 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.cts +22 -0
  50. package/dist/index.d.ts +22 -0
  51. package/dist/index.js +312 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/playground.cjs +364 -0
  54. package/dist/playground.cjs.map +1 -0
  55. package/dist/playground.d.cts +12 -0
  56. package/dist/playground.d.ts +12 -0
  57. package/dist/playground.js +337 -0
  58. package/dist/playground.js.map +1 -0
  59. package/dist/router-BVB_I-tu.d.ts +65 -0
  60. package/dist/router-Cikk8Heq.d.cts +65 -0
  61. package/dist/schema-BJsictSV.d.cts +172 -0
  62. package/dist/schema-BJsictSV.d.ts +172 -0
  63. package/package.json +150 -0
  64. package/templates/CLAUDE.md +71 -0
  65. package/templates/app/routes/auth/login.ts +35 -0
  66. package/templates/app/routes/health.ts +20 -0
  67. package/templates/app/schemas/user.ts +26 -0
  68. package/templates/clawfire.config.ts +25 -0
  69. package/templates/functions/index.ts +43 -0
  70. package/templates/starter/.claude/skills/clawfire-api/SKILL.md +131 -0
  71. package/templates/starter/.claude/skills/clawfire-auth/SKILL.md +111 -0
  72. package/templates/starter/.claude/skills/clawfire-deploy/SKILL.md +95 -0
  73. package/templates/starter/.claude/skills/clawfire-diagnose/SKILL.md +99 -0
  74. package/templates/starter/.claude/skills/clawfire-model/SKILL.md +128 -0
  75. package/templates/starter/CLAUDE.md +227 -0
  76. package/templates/starter/app/routes/health.ts +20 -0
  77. package/templates/starter/app/routes/todos/create.ts +25 -0
  78. package/templates/starter/app/routes/todos/delete.ts +20 -0
  79. package/templates/starter/app/routes/todos/list.ts +26 -0
  80. package/templates/starter/app/routes/todos/update.ts +32 -0
  81. package/templates/starter/app/schemas/todo.ts +16 -0
  82. package/templates/starter/app/store.ts +56 -0
  83. package/templates/starter/clawfire.config.ts +25 -0
  84. package/templates/starter/dev.ts +12 -0
  85. package/templates/starter/package.json +19 -0
  86. package/templates/starter/public/index.html +365 -0
  87. package/templates/starter/tsconfig.json +17 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dev/dev-server.ts","../src/core/schema.ts","../src/core/errors.ts","../src/core/logger.ts","../src/firebase/auth.ts","../src/routing/router.ts","../src/routing/discover.ts","../src/playground/html.ts","../src/dev/watcher.ts"],"sourcesContent":["/**\n * Clawfire Development Server\n *\n * 핫 리로드 지원 로컬 개발 서버\n * - 파일 변경 감지 → 라우트 자동 재로딩\n * - SSE 기반 브라우저 라이브 리로드\n * - Playground 자동 갱신\n * - 모델 변경 시 Rules/Indexes 자동 재생성\n */\nimport http from \"node:http\";\nimport { resolve, join, relative } from \"node:path\";\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\nimport type { APIContract } from \"../core/schema.js\";\nimport { ClawfireRouter, createRouter, type RouterOptions } from \"../routing/router.js\";\nimport { discoverRoutes, type DiscoveredRoute } from \"../routing/discover.js\";\nimport { generatePlaygroundHtml } from \"../playground/html.js\";\nimport { FileWatcher, type WatchEvent } from \"./watcher.js\";\nimport { logger } from \"../core/logger.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport interface DevServerOptions {\n /** 프로젝트 루트 경로 */\n projectDir?: string;\n /** 서버 포트 (기본: 3456) */\n port?: number;\n /** 라우터 옵션 */\n routerOptions?: Partial<RouterOptions>;\n /** 핫 리로드 활성화 (기본: true) */\n hotReload?: boolean;\n /** 변경 감지 디바운스 ms (기본: 150) */\n debounceMs?: number;\n /** 라우트를 수동으로 등록하는 콜백 (파일 기반 대신) */\n onSetupRoutes?: (router: ClawfireRouter) => void | Promise<void>;\n}\n\ninterface SSEClient {\n id: number;\n res: http.ServerResponse;\n}\n\n// ─── Dev Server ──────────────────────────────────────────────────────\n\nexport class DevServer {\n private server: http.Server | null = null;\n private router: ClawfireRouter;\n private watcher: FileWatcher | null = null;\n private sseClients: SSEClient[] = [];\n private sseIdCounter = 0;\n private options: Required<DevServerOptions>;\n private routesDir: string;\n private schemasDir: string;\n private playgroundHtml = \"\";\n private importCounter = 0; // ESM 캐시 버스팅용\n private isReloading = false;\n\n constructor(options: DevServerOptions = {}) {\n this.options = {\n projectDir: options.projectDir || process.cwd(),\n port: options.port || 3456,\n routerOptions: options.routerOptions || {},\n hotReload: options.hotReload !== false,\n debounceMs: options.debounceMs || 150,\n onSetupRoutes: options.onSetupRoutes || (() => {}),\n };\n\n this.routesDir = resolve(this.options.projectDir, \"app/routes\");\n this.schemasDir = resolve(this.options.projectDir, \"app/schemas\");\n\n this.router = createRouter({\n cors: [\"*\"], // dev에서는 모든 origin 허용\n rateLimit: 0, // dev에서는 rate limit 비활성화\n ...this.options.routerOptions,\n });\n }\n\n /**\n * 개발 서버 시작\n */\n async start(): Promise<void> {\n // 라우트 로딩\n await this.loadRoutes();\n\n // Playground HTML 생성 (라이브 리로드 스크립트 포함)\n this.regeneratePlayground();\n\n // HTTP 서버 생성\n this.server = http.createServer((req, res) => this.handleHttpRequest(req, res));\n\n // 파일 감시 시작\n if (this.options.hotReload) {\n this.startWatcher();\n }\n\n // 서버 시작\n await new Promise<void>((resolve, reject) => {\n this.server!.listen(this.options.port, () => {\n resolve();\n });\n this.server!.on(\"error\", reject);\n });\n\n this.printStartupBanner();\n }\n\n /**\n * 서버 종료\n */\n async stop(): Promise<void> {\n this.watcher?.close();\n for (const client of this.sseClients) {\n client.res.end();\n }\n this.sseClients = [];\n this.router.destroy();\n\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server!.close(() => resolve());\n });\n }\n }\n\n // ─── Route Loading ─────────────────────────────────────────────────\n\n /**\n * 라우트 파일 로딩 (또는 수동 콜백)\n */\n private async loadRoutes(): Promise<void> {\n // 새 라우터로 교체 (이전 라우트 정리)\n this.router.destroy();\n this.router = createRouter({\n cors: [\"*\"],\n rateLimit: 0,\n ...this.options.routerOptions,\n });\n\n // 수동 등록 콜백이 있으면 사용\n if (this.options.onSetupRoutes) {\n await this.options.onSetupRoutes(this.router);\n if (this.router.getRoutes().length > 0) return;\n }\n\n // 파일 기반 라우트 자동 발견\n if (!existsSync(this.routesDir)) return;\n\n const discovered = discoverRoutes(this.routesDir);\n for (const route of discovered) {\n try {\n const fullPath = resolve(this.routesDir, route.filePath);\n const fileUrl = pathToFileURL(fullPath).href;\n // ESM 캐시 버스팅: query parameter로 매번 새로 import\n const mod = await import(`${fileUrl}?v=${++this.importCounter}`);\n const contract = mod.default as APIContract;\n if (contract && typeof contract.handler === \"function\" && contract.input && contract.output && contract.meta) {\n this.router.register(route.apiPath, contract);\n }\n } catch (err) {\n logger.warn(`Failed to load route: ${route.filePath}`, err);\n }\n }\n }\n\n /**\n * 라우트 핫 리로드\n */\n private async reloadRoutes(event: WatchEvent): Promise<void> {\n if (this.isReloading) return;\n this.isReloading = true;\n\n const relPath = relative(this.options.projectDir, event.filePath);\n const timestamp = new Date().toLocaleTimeString();\n\n console.log(`\\n \\x1b[33m[${timestamp}]\\x1b[0m \\x1b[36m${relPath}\\x1b[0m changed`);\n console.log(\" Reloading routes...\");\n\n try {\n await this.loadRoutes();\n this.regeneratePlayground();\n\n const routeCount = this.router.getRoutes().length;\n console.log(` \\x1b[32m✓\\x1b[0m ${routeCount} routes loaded`);\n\n // SSE로 브라우저에 알림\n this.broadcastSSE({\n type: event.type,\n file: relPath,\n timestamp: event.timestamp,\n routes: routeCount,\n });\n } catch (err) {\n console.log(` \\x1b[31m✗\\x1b[0m Reload failed:`, err);\n this.broadcastSSE({\n type: \"error\",\n file: relPath,\n message: err instanceof Error ? err.message : \"Unknown error\",\n });\n } finally {\n this.isReloading = false;\n }\n }\n\n // ─── File Watcher ──────────────────────────────────────────────────\n\n private startWatcher(): void {\n this.watcher = new FileWatcher(this.options.debounceMs);\n\n // routes 감시\n if (existsSync(this.routesDir)) {\n this.watcher.watchDir(this.routesDir, \"route-change\");\n }\n\n // schemas 감시\n if (existsSync(this.schemasDir)) {\n this.watcher.watchDir(this.schemasDir, \"schema-change\");\n }\n\n // config 파일 감시\n const configFile = resolve(this.options.projectDir, \"clawfire.config.ts\");\n if (existsSync(configFile)) {\n this.watcher.watchFile(configFile, \"config-change\");\n }\n\n // 변경 이벤트 핸들러\n this.watcher.on(\"change\", (event: WatchEvent) => {\n this.reloadRoutes(event);\n });\n }\n\n // ─── SSE (Server-Sent Events) ──────────────────────────────────────\n\n private handleSSE(req: http.IncomingMessage, res: http.ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n\n const clientId = ++this.sseIdCounter;\n const client: SSEClient = { id: clientId, res };\n this.sseClients.push(client);\n\n // 연결 확인 이벤트\n res.write(`data: ${JSON.stringify({ type: \"connected\", id: clientId })}\\n\\n`);\n\n // 연결 종료 시 정리\n req.on(\"close\", () => {\n this.sseClients = this.sseClients.filter((c) => c.id !== clientId);\n });\n }\n\n private broadcastSSE(data: Record<string, unknown>): void {\n const message = `data: ${JSON.stringify(data)}\\n\\n`;\n for (const client of this.sseClients) {\n try {\n client.res.write(message);\n } catch {\n // 연결이 끊긴 클라이언트 무시\n }\n }\n }\n\n // ─── Playground ────────────────────────────────────────────────────\n\n private regeneratePlayground(): void {\n const baseHtml = generatePlaygroundHtml({\n title: \"Clawfire Dev Playground\",\n apiBaseUrl: `http://localhost:${this.options.port}`,\n });\n\n // 라이브 리로드 스크립트 주입\n const liveReloadScript = `\n<script>\n(function() {\n const banner = document.createElement('div');\n banner.id = 'dev-banner';\n banner.style.cssText = 'position:fixed;bottom:0;left:0;right:0;padding:6px 16px;background:#1a1a2e;color:#f97316;font-size:12px;font-family:monospace;z-index:9999;display:flex;align-items:center;gap:8px;border-top:1px solid #2a2a2a;';\n banner.innerHTML = '<span style=\"width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block;\" id=\"dev-dot\"></span><span>Clawfire Dev Server</span><span style=\"color:#666;margin-left:auto;\" id=\"dev-status\">Connected</span>';\n document.body.appendChild(banner);\n\n let reconnectTimer;\n function connect() {\n const es = new EventSource('http://localhost:${this.options.port}/__dev/events');\n\n es.onopen = () => {\n document.getElementById('dev-dot').style.background = '#22c55e';\n document.getElementById('dev-status').textContent = 'Connected';\n if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }\n };\n\n es.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n if (data.type === 'connected') return;\n\n if (data.type === 'error') {\n document.getElementById('dev-dot').style.background = '#ef4444';\n document.getElementById('dev-status').textContent = 'Error: ' + (data.message || 'reload failed');\n return;\n }\n\n // 파일 변경 → 페이지 새로고침\n document.getElementById('dev-dot').style.background = '#eab308';\n document.getElementById('dev-status').textContent = 'Reloading...';\n setTimeout(() => window.location.reload(), 300);\n } catch {}\n };\n\n es.onerror = () => {\n es.close();\n document.getElementById('dev-dot').style.background = '#ef4444';\n document.getElementById('dev-status').textContent = 'Disconnected — reconnecting...';\n reconnectTimer = setTimeout(connect, 2000);\n };\n }\n\n connect();\n})();\n</script>`;\n\n this.playgroundHtml = baseHtml.replace(\"</body>\", liveReloadScript + \"\\n</body>\");\n }\n\n // ─── HTTP Request Handler ──────────────────────────────────────────\n\n private handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || \"/\", `http://localhost:${this.options.port}`);\n\n // SSE 엔드포인트\n if (url.pathname === \"/__dev/events\") {\n this.handleSSE(req, res);\n return;\n }\n\n // Playground\n if (url.pathname === \"/\" || url.pathname === \"/__playground\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(this.playgroundHtml);\n return;\n }\n\n // 정적 파일 (public/ 디렉터리)\n if (!url.pathname.startsWith(\"/api\") && !url.pathname.startsWith(\"/__\")) {\n const publicDir = resolve(this.options.projectDir, \"public\");\n const filePath = resolve(publicDir, url.pathname.slice(1));\n if (existsSync(filePath) && !filePath.includes(\"..\")) {\n try {\n const content = readFileSync(filePath);\n const ext = filePath.split(\".\").pop() || \"\";\n const mimeTypes: Record<string, string> = {\n html: \"text/html\", css: \"text/css\", js: \"application/javascript\",\n json: \"application/json\", png: \"image/png\", jpg: \"image/jpeg\",\n svg: \"image/svg+xml\", ico: \"image/x-icon\", woff2: \"font/woff2\",\n };\n res.writeHead(200, { \"Content-Type\": mimeTypes[ext] || \"application/octet-stream\" });\n res.end(content);\n return;\n } catch {}\n }\n }\n\n // API 요청\n if (url.pathname.startsWith(\"/api\")) {\n // CORS\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n let body = \"\";\n req.on(\"data\", (chunk) => { body += chunk; });\n req.on(\"end\", async () => {\n let parsed = {};\n try { parsed = body ? JSON.parse(body) : {}; } catch {}\n\n await this.router.handleRequest(\n {\n method: req.method,\n path: url.pathname,\n body: parsed,\n headers: req.headers as Record<string, string>,\n ip: req.socket.remoteAddress || \"127.0.0.1\",\n },\n {\n set(h: Record<string, string>) {\n for (const [k, v] of Object.entries(h)) {\n try { res.setHeader(k, v); } catch {}\n }\n },\n status(code: number) {\n return {\n json(data: unknown) {\n res.writeHead(code, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n },\n send(data: string) { res.writeHead(code); res.end(data); },\n end() { res.writeHead(code); res.end(); },\n };\n },\n },\n );\n });\n return;\n }\n\n // 404\n res.writeHead(404);\n res.end(\"Not found\");\n }\n\n // ─── Startup Banner ────────────────────────────────────────────────\n\n private printStartupBanner(): void {\n const routes = this.router.getRoutes();\n const watching = this.options.hotReload;\n\n console.log(\"\");\n console.log(\" \\x1b[1m\\x1b[33m⚡ Clawfire Dev Server\\x1b[0m\");\n console.log(\" \\x1b[2m─────────────────────────────────────\\x1b[0m\");\n console.log(` \\x1b[36mPlayground\\x1b[0m : http://localhost:${this.options.port}`);\n console.log(` \\x1b[36mAPI\\x1b[0m : http://localhost:${this.options.port}/api/...`);\n console.log(` \\x1b[36mManifest\\x1b[0m : http://localhost:${this.options.port}/api/__manifest`);\n console.log(\"\");\n console.log(` \\x1b[32mRoutes (${routes.length})\\x1b[0m:`);\n for (const route of routes) {\n const auth = route.contract.meta.auth || \"public\";\n const authColor = auth === \"public\" ? \"32\" : auth === \"authenticated\" ? \"34\" : auth === \"role\" ? \"33\" : \"31\";\n console.log(` POST /api\\x1b[1m${route.path}\\x1b[0m \\x1b[${authColor}m[${auth}]\\x1b[0m \\x1b[2m${route.contract.meta.description}\\x1b[0m`);\n }\n console.log(\"\");\n if (watching) {\n console.log(` \\x1b[35mHot Reload\\x1b[0m : \\x1b[32mON\\x1b[0m`);\n console.log(` \\x1b[2mWatching: app/routes/, app/schemas/\\x1b[0m`);\n } else {\n console.log(` \\x1b[35mHot Reload\\x1b[0m : OFF`);\n }\n console.log(`\\n \\x1b[2mPress Ctrl+C to stop\\x1b[0m\\n`);\n }\n}\n\n/**\n * 개발 서버 생성 및 시작 (원라인 헬퍼)\n *\n * @example\n * ```ts\n * import { startDevServer } from \"clawfire/dev\";\n *\n * startDevServer({\n * port: 3456,\n * onSetupRoutes: (router) => {\n * router.register(\"/health\", healthRoute);\n * router.register(\"/products/list\", listRoute);\n * }\n * });\n * ```\n */\nexport async function startDevServer(options?: DevServerOptions): Promise<DevServer> {\n const server = new DevServer(options);\n await server.start();\n\n // graceful shutdown\n const shutdown = async () => {\n console.log(\"\\n Shutting down...\");\n await server.stop();\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n return server;\n}\n","/**\n * Clawfire Schema & Contract System\n *\n * 모든 API는 input/output schema + meta + handler로 구성된 \"계약(Contract)\"으로 정의됩니다.\n * Zod를 사용하여 타입 안전성과 런타임 검증을 동시에 보장합니다.\n */\nimport { z, type ZodType, type ZodObject, type ZodRawShape } from \"zod\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\n/** 인증 컨텍스트 */\nexport interface AuthContext {\n uid: string;\n email?: string;\n emailVerified?: boolean;\n role?: string;\n customClaims?: Record<string, unknown>;\n token?: string;\n}\n\n/** API 핸들러에 전달되는 컨텍스트 */\nexport interface HandlerContext {\n auth: AuthContext | null;\n /** 재인증 여부 (민감 작업용) */\n reauthenticated?: boolean;\n /** 원본 요청 헤더 */\n headers?: Record<string, string>;\n /** 요청 IP */\n ip?: string;\n}\n\n/** 권한 수준 */\nexport type AuthLevel = \"public\" | \"authenticated\" | \"role\" | \"reauth\";\n\n/** API 메타데이터 */\nexport interface APIMeta {\n /** API 설명 (AI/Playground용) */\n description: string;\n /** 태그 (그룹화용) */\n tags?: string[];\n /** 인증 요구 수준 */\n auth?: AuthLevel;\n /** 필요 역할 (auth가 'role'일 때) */\n roles?: string[];\n /** 재인증 필요 여부 */\n reauth?: boolean;\n /** Rate limit (초당 요청 수) */\n rateLimit?: number;\n /** 비활성화 여부 */\n deprecated?: boolean;\n /** 예시 입력값 */\n exampleInput?: unknown;\n /** 예시 출력값 */\n exampleOutput?: unknown;\n}\n\n/** API 계약 정의 */\nexport interface APIContract<\n TInput extends ZodType = ZodType,\n TOutput extends ZodType = ZodType,\n> {\n /** 입력 스키마 */\n input: TInput;\n /** 출력 스키마 */\n output: TOutput;\n /** 메타데이터 */\n meta: APIMeta;\n /** 핸들러 함수 */\n handler: (\n input: z.infer<TInput>,\n ctx: HandlerContext,\n ) => Promise<z.infer<TOutput>>;\n}\n\n/** 모델 필드 정의 */\nexport interface ModelField {\n type: \"string\" | \"number\" | \"boolean\" | \"timestamp\" | \"array\" | \"map\" | \"reference\" | \"geopoint\";\n required?: boolean;\n description?: string;\n default?: unknown;\n /** 배열 아이템 타입 */\n items?: ModelField;\n /** 맵 값 타입 */\n values?: ModelField;\n /** 참조 대상 컬렉션 */\n ref?: string;\n /** enum 값 리스트 */\n enum?: string[];\n}\n\n/** 모델 정의 */\nexport interface ModelDefinition {\n /** 컬렉션 이름 */\n collection: string;\n /** 필드 정의 */\n fields: Record<string, ModelField>;\n /** 서브컬렉션 */\n subcollections?: Record<string, ModelDefinition>;\n /** 인덱스 */\n indexes?: ModelIndex[];\n /** 보안 규칙 */\n rules?: ModelRules;\n /** 타임스탬프 자동 생성 */\n timestamps?: boolean;\n /** 소프트 삭제 */\n softDelete?: boolean;\n}\n\n/** Firestore 인덱스 */\nexport interface ModelIndex {\n fields: Array<{ field: string; order?: \"asc\" | \"desc\" }>;\n}\n\n/** 모델 보안 규칙 */\nexport interface ModelRules {\n read?: AuthLevel;\n create?: AuthLevel;\n update?: AuthLevel;\n delete?: AuthLevel;\n readRoles?: string[];\n createRoles?: string[];\n updateRoles?: string[];\n deleteRoles?: string[];\n /** 소유자만 읽기/쓰기 가능 필드 */\n ownerField?: string;\n}\n\n// ─── Builders ────────────────────────────────────────────────────────\n\n/**\n * API 계약 정의\n *\n * @example\n * ```ts\n * export default defineAPI({\n * input: z.object({ name: z.string() }),\n * output: z.object({ id: z.string(), name: z.string() }),\n * meta: { description: \"상품 생성\", auth: \"authenticated\" },\n * handler: async (input, ctx) => {\n * const id = await db.create(\"products\", input);\n * return { id, ...input };\n * }\n * });\n * ```\n */\nexport function defineAPI<\n TInput extends ZodType,\n TOutput extends ZodType,\n>(contract: APIContract<TInput, TOutput>): APIContract<TInput, TOutput> {\n return contract;\n}\n\n/**\n * 모델(Firestore 컬렉션) 정의\n *\n * @example\n * ```ts\n * export const Product = defineModel({\n * collection: \"products\",\n * fields: {\n * name: { type: \"string\", required: true },\n * price: { type: \"number\", required: true },\n * tags: { type: \"array\", items: { type: \"string\" } },\n * },\n * timestamps: true,\n * rules: { read: \"public\", create: \"authenticated\" }\n * });\n * ```\n */\nexport function defineModel(definition: ModelDefinition): ModelDefinition {\n return {\n timestamps: true,\n ...definition,\n };\n}\n\n// ─── Schema Utilities ────────────────────────────────────────────────\n\n/** Zod 스키마에서 JSON Schema 생성 (Playground/문서용) */\nexport function zodToJsonSchema(schema: ZodType): Record<string, unknown> {\n return extractZodShape(schema);\n}\n\nfunction extractZodShape(schema: ZodType): Record<string, unknown> {\n const def = (schema as any)._def;\n\n if (!def) return { type: \"unknown\" };\n\n switch (def.typeName) {\n case \"ZodObject\": {\n const shape = (schema as ZodObject<ZodRawShape>).shape;\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = extractZodShape(value as ZodType);\n if (!(value as any).isOptional?.()) {\n const innerDef = (value as any)._def;\n if (innerDef?.typeName !== \"ZodOptional\" && innerDef?.typeName !== \"ZodDefault\") {\n required.push(key);\n }\n }\n }\n\n return { type: \"object\", properties, ...(required.length > 0 ? { required } : {}) };\n }\n case \"ZodString\":\n return { type: \"string\", ...(def.checks?.length ? extractStringChecks(def.checks) : {}) };\n case \"ZodNumber\":\n return { type: \"number\" };\n case \"ZodBoolean\":\n return { type: \"boolean\" };\n case \"ZodArray\":\n return { type: \"array\", items: extractZodShape(def.type) };\n case \"ZodEnum\":\n return { type: \"string\", enum: def.values };\n case \"ZodOptional\":\n return { ...extractZodShape(def.innerType), optional: true };\n case \"ZodDefault\":\n return { ...extractZodShape(def.innerType), default: def.defaultValue() };\n case \"ZodNullable\":\n return { ...extractZodShape(def.innerType), nullable: true };\n case \"ZodLiteral\":\n return { type: typeof def.value, const: def.value };\n case \"ZodUnion\":\n return { oneOf: def.options.map((o: ZodType) => extractZodShape(o)) };\n case \"ZodRecord\":\n return { type: \"object\", additionalProperties: extractZodShape(def.valueType) };\n case \"ZodDate\":\n return { type: \"string\", format: \"date-time\" };\n default:\n return { type: \"unknown\" };\n }\n}\n\nfunction extractStringChecks(checks: Array<{ kind: string; value?: unknown }>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const check of checks) {\n switch (check.kind) {\n case \"min\": result.minLength = check.value; break;\n case \"max\": result.maxLength = check.value; break;\n case \"email\": result.format = \"email\"; break;\n case \"url\": result.format = \"uri\"; break;\n case \"uuid\": result.format = \"uuid\"; break;\n }\n }\n return result;\n}\n\n/** 모델 정의에서 Zod 스키마 자동 생성 */\nexport function modelToZodSchema(model: ModelDefinition): ZodObject<ZodRawShape> {\n const shape: ZodRawShape = {};\n\n for (const [key, field] of Object.entries(model.fields)) {\n let fieldSchema: ZodType = fieldToZod(field);\n if (!field.required) {\n fieldSchema = fieldSchema.optional();\n }\n shape[key] = fieldSchema;\n }\n\n if (model.timestamps) {\n shape.createdAt = z.string().datetime().optional();\n shape.updatedAt = z.string().datetime().optional();\n }\n\n if (model.softDelete) {\n shape.deletedAt = z.string().datetime().nullable().optional();\n }\n\n return z.object(shape);\n}\n\nfunction fieldToZod(field: ModelField): ZodType {\n switch (field.type) {\n case \"string\":\n if (field.enum) return z.enum(field.enum as [string, ...string[]]);\n return z.string();\n case \"number\":\n return z.number();\n case \"boolean\":\n return z.boolean();\n case \"timestamp\":\n return z.string().datetime();\n case \"array\":\n if (field.items) return z.array(fieldToZod(field.items));\n return z.array(z.unknown());\n case \"map\":\n if (field.values) return z.record(z.string(), fieldToZod(field.values));\n return z.record(z.string(), z.unknown());\n case \"reference\":\n return z.string(); // 참조는 문서 경로 문자열\n case \"geopoint\":\n return z.object({ latitude: z.number(), longitude: z.number() });\n default:\n return z.unknown();\n }\n}\n\n// ─── Manifest ────────────────────────────────────────────────────────\n\n/** API 매니페스트 항목 */\nexport interface ManifestEntry {\n path: string;\n method: \"POST\"; // Clawfire는 모두 POST\n meta: APIMeta;\n inputSchema: Record<string, unknown>;\n outputSchema: Record<string, unknown>;\n}\n\n/** 전체 매니페스트 */\nexport interface Manifest {\n version: string;\n generatedAt: string;\n apis: ManifestEntry[];\n models: Record<string, ModelDefinition>;\n}\n\n/** 계약에서 매니페스트 항목 생성 */\nexport function contractToManifest(\n path: string,\n contract: APIContract,\n): ManifestEntry {\n return {\n path,\n method: \"POST\",\n meta: contract.meta,\n inputSchema: zodToJsonSchema(contract.input),\n outputSchema: zodToJsonSchema(contract.output),\n };\n}\n","/**\n * Clawfire Error System\n *\n * 표준화된 에러 코드와 구조로 일관된 에러 응답을 제공합니다.\n */\n\nexport type ClawfireErrorCode =\n | \"VALIDATION_ERROR\"\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"RATE_LIMITED\"\n | \"REAUTH_REQUIRED\"\n | \"INTERNAL_ERROR\"\n | \"SERVICE_UNAVAILABLE\";\n\nconst HTTP_STATUS_MAP: Record<ClawfireErrorCode, number> = {\n VALIDATION_ERROR: 400,\n UNAUTHORIZED: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n RATE_LIMITED: 429,\n REAUTH_REQUIRED: 401,\n INTERNAL_ERROR: 500,\n SERVICE_UNAVAILABLE: 503,\n};\n\nexport class ClawfireError extends Error {\n readonly code: ClawfireErrorCode;\n readonly statusCode: number;\n readonly details?: unknown;\n\n constructor(code: ClawfireErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = \"ClawfireError\";\n this.code = code;\n this.statusCode = HTTP_STATUS_MAP[code];\n this.details = details;\n }\n\n toJSON() {\n return {\n error: {\n code: this.code,\n message: this.message,\n ...(this.details ? { details: this.details } : {}),\n },\n };\n }\n}\n\n/** 편의 팩토리 함수 */\nexport const Errors = {\n validation: (message: string, details?: unknown) =>\n new ClawfireError(\"VALIDATION_ERROR\", message, details),\n\n unauthorized: (message = \"Authentication required\") =>\n new ClawfireError(\"UNAUTHORIZED\", message),\n\n forbidden: (message = \"Insufficient permissions\") =>\n new ClawfireError(\"FORBIDDEN\", message),\n\n notFound: (message = \"Resource not found\") =>\n new ClawfireError(\"NOT_FOUND\", message),\n\n conflict: (message: string) =>\n new ClawfireError(\"CONFLICT\", message),\n\n rateLimited: (message = \"Too many requests\") =>\n new ClawfireError(\"RATE_LIMITED\", message),\n\n reauthRequired: (message = \"Re-authentication required for this action\") =>\n new ClawfireError(\"REAUTH_REQUIRED\", message),\n\n internal: (message = \"Internal server error\") =>\n new ClawfireError(\"INTERNAL_ERROR\", message),\n\n unavailable: (message = \"Service temporarily unavailable\") =>\n new ClawfireError(\"SERVICE_UNAVAILABLE\", message),\n};\n","/**\n * Clawfire Logger\n *\n * 보안 로깅 (민감 정보 마스킹 포함)\n */\n\n/** 마스킹할 필드명 패턴 */\nconst SENSITIVE_FIELDS = [\n \"password\",\n \"token\",\n \"secret\",\n \"apiKey\",\n \"api_key\",\n \"authorization\",\n \"credit_card\",\n \"creditCard\",\n \"ssn\",\n \"cardNumber\",\n \"card_number\",\n \"cvv\",\n \"pin\",\n];\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nlet currentLevel: LogLevel = \"info\";\n\nexport function setLogLevel(level: LogLevel) {\n currentLevel = level;\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];\n}\n\n/** 민감 필드 마스킹 */\nexport function maskSensitive(obj: unknown): unknown {\n if (obj === null || obj === undefined) return obj;\n if (typeof obj === \"string\") return obj;\n if (typeof obj !== \"object\") return obj;\n\n if (Array.isArray(obj)) {\n return obj.map(maskSensitive);\n }\n\n const masked: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n if (SENSITIVE_FIELDS.some((f) => key.toLowerCase().includes(f.toLowerCase()))) {\n masked[key] = \"***MASKED***\";\n } else if (typeof value === \"object\" && value !== null) {\n masked[key] = maskSensitive(value);\n } else {\n masked[key] = value;\n }\n }\n return masked;\n}\n\nfunction formatLog(level: LogLevel, message: string, data?: unknown): string {\n const timestamp = new Date().toISOString();\n const prefix = `[${timestamp}] [CLAWFIRE] [${level.toUpperCase()}]`;\n if (data !== undefined) {\n return `${prefix} ${message} ${JSON.stringify(maskSensitive(data))}`;\n }\n return `${prefix} ${message}`;\n}\n\nexport const logger = {\n debug(message: string, data?: unknown) {\n if (shouldLog(\"debug\")) console.debug(formatLog(\"debug\", message, data));\n },\n info(message: string, data?: unknown) {\n if (shouldLog(\"info\")) console.info(formatLog(\"info\", message, data));\n },\n warn(message: string, data?: unknown) {\n if (shouldLog(\"warn\")) console.warn(formatLog(\"warn\", message, data));\n },\n error(message: string, data?: unknown) {\n if (shouldLog(\"error\")) console.error(formatLog(\"error\", message, data));\n },\n};\n","/**\n * Clawfire Auth Helpers\n *\n * Firebase Auth 통합: 토큰 검증, 역할 관리, 재인증\n */\nimport type { AuthContext, AuthLevel } from \"../core/schema.js\";\nimport { Errors } from \"../core/errors.js\";\n\n// ─── Server-side Auth (Admin SDK) ────────────────────────────────────\n\n/**\n * Firebase ID 토큰에서 AuthContext 추출 (서버사이드)\n */\nexport async function verifyToken(\n auth: any, // firebase-admin Auth instance\n idToken: string,\n): Promise<AuthContext> {\n const decoded = await auth.verifyIdToken(idToken);\n return {\n uid: decoded.uid,\n email: decoded.email,\n emailVerified: decoded.email_verified,\n role: decoded.role || decoded.customClaims?.role,\n customClaims: decoded,\n token: idToken,\n };\n}\n\n/**\n * 재인증 토큰 검증 (최근 5분 이내 인증)\n */\nexport async function verifyReauth(\n auth: any,\n idToken: string,\n maxAgeSeconds = 300,\n): Promise<AuthContext> {\n const decoded = await auth.verifyIdToken(idToken, true);\n const authTime = decoded.auth_time * 1000;\n const now = Date.now();\n\n if (now - authTime > maxAgeSeconds * 1000) {\n throw Errors.reauthRequired(\n `Re-authentication required. Last auth was ${Math.floor((now - authTime) / 1000)}s ago.`,\n );\n }\n\n return {\n uid: decoded.uid,\n email: decoded.email,\n emailVerified: decoded.email_verified,\n role: decoded.role || decoded.customClaims?.role,\n customClaims: decoded,\n token: idToken,\n };\n}\n\n/**\n * 요청에서 Authorization 헤더의 Bearer 토큰 추출\n */\nexport function extractBearerToken(authHeader?: string): string | null {\n if (!authHeader) return null;\n const parts = authHeader.split(\" \");\n if (parts.length !== 2 || parts[0] !== \"Bearer\") return null;\n return parts[1];\n}\n\n/**\n * 인증 수준 체크\n */\nexport function checkAuthLevel(\n authCtx: AuthContext | null,\n level: AuthLevel,\n roles?: string[],\n reauthenticated?: boolean,\n): void {\n switch (level) {\n case \"public\":\n return; // 누구나 접근 가능\n\n case \"authenticated\":\n if (!authCtx) throw Errors.unauthorized();\n return;\n\n case \"role\":\n if (!authCtx) throw Errors.unauthorized();\n if (!roles || roles.length === 0) return;\n if (!authCtx.role || !roles.includes(authCtx.role)) {\n throw Errors.forbidden(`Required role: ${roles.join(\" or \")}`);\n }\n return;\n\n case \"reauth\":\n if (!authCtx) throw Errors.unauthorized();\n if (!reauthenticated) {\n throw Errors.reauthRequired();\n }\n return;\n }\n}\n\n// ─── Custom Claims / Role Management ─────────────────────────────────\n\n/**\n * 사용자에게 역할(Role) 설정\n */\nexport async function setUserRole(\n auth: any,\n uid: string,\n role: string,\n): Promise<void> {\n const user = await auth.getUser(uid);\n const currentClaims = user.customClaims || {};\n await auth.setCustomUserClaims(uid, { ...currentClaims, role });\n}\n\n/**\n * 사용자의 역할 조회\n */\nexport async function getUserRole(auth: any, uid: string): Promise<string | undefined> {\n const user = await auth.getUser(uid);\n return user.customClaims?.role;\n}\n\n/**\n * 사용자에게 커스텀 클레임 설정\n */\nexport async function setCustomClaims(\n auth: any,\n uid: string,\n claims: Record<string, unknown>,\n): Promise<void> {\n const user = await auth.getUser(uid);\n const currentClaims = user.customClaims || {};\n await auth.setCustomUserClaims(uid, { ...currentClaims, ...claims });\n}\n\n// ─── Client-side Auth Helpers ────────────────────────────────────────\n\n/**\n * 클라이언트 사이드 Auth 헬퍼 (firebase/auth 래핑)\n */\nexport function createClientAuth(firebaseAuth: any) {\n return {\n /** 현재 사용자 조회 */\n getCurrentUser(): any | null {\n return firebaseAuth.currentUser;\n },\n\n /** ID 토큰 취득 */\n async getIdToken(forceRefresh = false): Promise<string | null> {\n const user = firebaseAuth.currentUser;\n if (!user) return null;\n return user.getIdToken(forceRefresh);\n },\n\n /** 인증 상태 변경 리스너 */\n onAuthStateChanged(callback: (user: any | null) => void): () => void {\n return firebaseAuth.onAuthStateChanged(callback);\n },\n\n /** 로그아웃 */\n async signOut(): Promise<void> {\n await firebaseAuth.signOut();\n },\n\n /** 원시 auth 접근 */\n raw: firebaseAuth,\n };\n}\n\nexport type ClientAuth = ReturnType<typeof createClientAuth>;\n","/**\n * Clawfire File-based Router\n *\n * app/routes/ 디렉터리 구조를 자동으로 API 경로로 매핑합니다.\n *\n * 파일 구조 → API 경로:\n * app/routes/products/list.ts → /api/products/list\n * app/routes/products/create.ts → /api/products/create\n * app/routes/auth/login.ts → /api/auth/login\n * app/routes/users/[id]/get.ts → /api/users/:id/get\n */\nimport { z } from \"zod\";\nimport type { APIContract, HandlerContext, AuthContext, Manifest, ManifestEntry } from \"../core/schema.js\";\nimport { contractToManifest, zodToJsonSchema } from \"../core/schema.js\";\nimport { ClawfireError, Errors } from \"../core/errors.js\";\nimport { logger } from \"../core/logger.js\";\nimport { checkAuthLevel, extractBearerToken, verifyToken, verifyReauth } from \"../firebase/auth.js\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport interface Route {\n path: string;\n contract: APIContract;\n}\n\nexport interface RouterOptions {\n /** Firebase Admin Auth 인스턴스 */\n auth?: any;\n /** Firebase Admin Firestore 인스턴스 */\n firestore?: any;\n /** CORS 허용 도메인 */\n cors?: string[];\n /** 전역 rate limit */\n rateLimit?: number;\n /** 전역 미들웨어 */\n middleware?: Middleware[];\n /** 플레이그라운드 활성화 */\n playground?: boolean;\n}\n\nexport type Middleware = (\n input: unknown,\n ctx: HandlerContext,\n next: () => Promise<unknown>,\n) => Promise<unknown>;\n\n// ─── Rate Limiter (in-memory) ────────────────────────────────────────\n\nclass RateLimiter {\n private store = new Map<string, { count: number; resetAt: number }>();\n private defaultLimit: number;\n\n constructor(defaultLimit: number) {\n this.defaultLimit = defaultLimit;\n }\n\n check(key: string, limit?: number): boolean {\n const max = limit || this.defaultLimit;\n const now = Date.now();\n const entry = this.store.get(key);\n\n if (!entry || now > entry.resetAt) {\n this.store.set(key, { count: 1, resetAt: now + 60000 }); // 1분 윈도우\n return true;\n }\n\n if (entry.count >= max) {\n return false;\n }\n\n entry.count++;\n return true;\n }\n\n // 오래된 엔트리 정리\n cleanup() {\n const now = Date.now();\n for (const [key, entry] of this.store) {\n if (now > entry.resetAt) this.store.delete(key);\n }\n }\n}\n\n// ─── Router ──────────────────────────────────────────────────────────\n\nexport class ClawfireRouter {\n private routes = new Map<string, Route>();\n private options: RouterOptions;\n private rateLimiter: RateLimiter;\n private cleanupInterval?: ReturnType<typeof setInterval>;\n\n constructor(options: RouterOptions = {}) {\n this.options = options;\n this.rateLimiter = new RateLimiter(options.rateLimit || 100);\n this.cleanupInterval = setInterval(() => this.rateLimiter.cleanup(), 60000);\n }\n\n /** 라우트 등록 */\n register(path: string, contract: APIContract): this {\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n this.routes.set(normalizedPath, { path: normalizedPath, contract });\n logger.debug(`Route registered: ${normalizedPath}`);\n return this;\n }\n\n /** 여러 라우트 한 번에 등록 */\n registerAll(routes: Record<string, APIContract>): this {\n for (const [path, contract] of Object.entries(routes)) {\n this.register(path, contract);\n }\n return this;\n }\n\n /** 매니페스트 생성 */\n getManifest(): Manifest {\n const apis: ManifestEntry[] = [];\n for (const [path, route] of this.routes) {\n apis.push(contractToManifest(path, route.contract));\n }\n\n return {\n version: \"1.0.0\",\n generatedAt: new Date().toISOString(),\n apis,\n models: {},\n };\n }\n\n /** HTTP 요청 핸들러 (Firebase Functions에서 사용) */\n async handleRequest(\n req: { path?: string; url?: string; body?: unknown; headers?: Record<string, string>; ip?: string; method?: string },\n res: { status: (code: number) => { json: (data: unknown) => void; send: (data: string) => void; end: () => void }; set: (headers: Record<string, string>) => void },\n ): Promise<void> {\n // CORS 처리\n const corsOrigins = this.options.cors || [];\n const origin = req.headers?.origin || req.headers?.Origin || \"\";\n\n if (corsOrigins.length > 0 && corsOrigins.includes(origin)) {\n res.set({\n \"Access-Control-Allow-Origin\": origin,\n \"Access-Control-Allow-Methods\": \"POST, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\",\n \"Access-Control-Max-Age\": \"86400\",\n });\n } else if (corsOrigins.length === 0) {\n // CORS 기본 deny — 설정 없으면 허용하지 않음\n res.set({\n \"Access-Control-Allow-Origin\": \"\",\n \"Access-Control-Allow-Methods\": \"POST, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\",\n });\n }\n\n // OPTIONS (preflight)\n if (req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n\n // 안전 헤더\n res.set({\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"X-XSS-Protection\": \"1; mode=block\",\n \"Content-Type\": \"application/json\",\n });\n\n // 경로 추출 — /api/xxx/yyy → /xxx/yyy\n let routePath = req.path || req.url || \"\";\n if (routePath.startsWith(\"/api\")) {\n routePath = routePath.slice(4);\n }\n\n // 시스템 엔드포인트 (GET 허용)\n if (routePath === \"/__manifest\") {\n res.status(200).json(this.getManifest());\n return;\n }\n\n // POST만 허용 (시스템 엔드포인트 제외)\n if (req.method && req.method !== \"POST\") {\n res.status(405).json({ error: { code: \"METHOD_NOT_ALLOWED\", message: \"Only POST is allowed\" } });\n return;\n }\n\n // 라우트 매칭\n const route = this.matchRoute(routePath);\n if (!route) {\n res.status(404).json({ error: { code: \"NOT_FOUND\", message: `API not found: ${routePath}` } });\n return;\n }\n\n try {\n // Rate limit\n const clientKey = req.ip || \"unknown\";\n const rateLimit = route.contract.meta.rateLimit || this.options.rateLimit;\n if (rateLimit && !this.rateLimiter.check(clientKey, rateLimit)) {\n throw Errors.rateLimited();\n }\n\n // Auth 처리\n let authCtx: AuthContext | null = null;\n let reauthenticated = false;\n const authHeader = req.headers?.authorization || req.headers?.Authorization;\n\n if (authHeader && this.options.auth) {\n const token = extractBearerToken(authHeader as string);\n if (token) {\n if (route.contract.meta.reauth || route.contract.meta.auth === \"reauth\") {\n authCtx = await verifyReauth(this.options.auth, token);\n reauthenticated = true;\n } else {\n authCtx = await verifyToken(this.options.auth, token);\n }\n }\n }\n\n // 권한 체크\n if (route.contract.meta.auth) {\n checkAuthLevel(authCtx, route.contract.meta.auth, route.contract.meta.roles, reauthenticated);\n }\n\n // 입력 검증\n const rawInput = req.body || {};\n const parsed = route.contract.input.safeParse(rawInput);\n if (!parsed.success) {\n throw Errors.validation(\"Invalid input\", parsed.error.flatten());\n }\n\n // 핸들러 컨텍스트\n const ctx: HandlerContext = {\n auth: authCtx,\n reauthenticated,\n headers: req.headers as Record<string, string>,\n ip: req.ip,\n };\n\n // 미들웨어 + 핸들러 실행\n let result: unknown;\n if (this.options.middleware && this.options.middleware.length > 0) {\n result = await this.runMiddleware(\n this.options.middleware,\n parsed.data,\n ctx,\n () => route.contract.handler(parsed.data, ctx),\n );\n } else {\n result = await route.contract.handler(parsed.data, ctx);\n }\n\n // 성공 응답\n res.status(200).json({ data: result });\n } catch (err) {\n if (err instanceof ClawfireError) {\n logger.warn(`API error [${err.code}]: ${err.message}`);\n res.status(err.statusCode).json(err.toJSON());\n } else {\n logger.error(\"Unhandled error\", err);\n res.status(500).json({\n error: {\n code: \"INTERNAL_ERROR\",\n message: \"An unexpected error occurred\",\n },\n });\n }\n }\n }\n\n /** 라우트 매칭 (동적 파라미터 지원) */\n private matchRoute(path: string): Route | undefined {\n // 정확한 매칭\n const exact = this.routes.get(path);\n if (exact) return exact;\n\n // 동적 파라미터 매칭 (/users/:id/get → /users/abc123/get)\n for (const [routePath, route] of this.routes) {\n if (this.matchDynamicRoute(routePath, path)) {\n return route;\n }\n }\n\n return undefined;\n }\n\n private matchDynamicRoute(pattern: string, actual: string): boolean {\n const patternParts = pattern.split(\"/\").filter(Boolean);\n const actualParts = actual.split(\"/\").filter(Boolean);\n if (patternParts.length !== actualParts.length) return false;\n\n return patternParts.every(\n (part, i) => part.startsWith(\":\") || part.startsWith(\"[\") || part === actualParts[i],\n );\n }\n\n /** 미들웨어 체인 실행 */\n private async runMiddleware(\n middlewares: Middleware[],\n input: unknown,\n ctx: HandlerContext,\n handler: () => Promise<unknown>,\n ): Promise<unknown> {\n let index = 0;\n const next = async (): Promise<unknown> => {\n if (index >= middlewares.length) {\n return handler();\n }\n const mw = middlewares[index++];\n return mw(input, ctx, next);\n };\n return next();\n }\n\n /** 등록된 라우트 목록 */\n getRoutes(): Route[] {\n return Array.from(this.routes.values());\n }\n\n /** 리소스 정리 */\n destroy() {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n }\n }\n}\n\n/**\n * 라우터 생성 헬퍼\n */\nexport function createRouter(options?: RouterOptions): ClawfireRouter {\n return new ClawfireRouter(options);\n}\n","/**\n * Clawfire Route Discovery\n *\n * 파일 시스템에서 라우트 자동 발견 (빌드 타임 & 런타임)\n */\nimport { resolve, relative, join } from \"path\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\n\nexport interface DiscoveredRoute {\n /** 파일 경로 (상대) */\n filePath: string;\n /** API 경로 (/products/list) */\n apiPath: string;\n /** 동적 파라미터 이름 */\n params: string[];\n}\n\n/**\n * routes 디렉터리에서 라우트 파일 자동 발견\n *\n * @param routesDir - routes 디렉터리 절대 경로\n * @returns 발견된 라우트 목록\n *\n * @example\n * ```\n * app/routes/products/list.ts → { apiPath: \"/products/list\", params: [] }\n * app/routes/products/[id]/get.ts → { apiPath: \"/products/:id/get\", params: [\"id\"] }\n * app/routes/health.ts → { apiPath: \"/health\", params: [] }\n * ```\n */\nexport function discoverRoutes(routesDir: string): DiscoveredRoute[] {\n if (!existsSync(routesDir)) {\n return [];\n }\n\n const routes: DiscoveredRoute[] = [];\n scanDirectory(routesDir, routesDir, routes);\n return routes.sort((a, b) => a.apiPath.localeCompare(b.apiPath));\n}\n\nfunction scanDirectory(baseDir: string, currentDir: string, routes: DiscoveredRoute[]): void {\n const entries = readdirSync(currentDir);\n\n for (const entry of entries) {\n const fullPath = join(currentDir, entry);\n const stat = statSync(fullPath);\n\n if (stat.isDirectory()) {\n // 숨김 디렉터리, node_modules 무시\n if (entry.startsWith(\".\") || entry === \"node_modules\") continue;\n scanDirectory(baseDir, fullPath, routes);\n } else if (stat.isFile()) {\n // .ts, .js 파일만\n if (!entry.endsWith(\".ts\") && !entry.endsWith(\".js\")) continue;\n // index, _로 시작하는 파일 무시\n if (entry.startsWith(\"_\")) continue;\n // .d.ts 무시\n if (entry.endsWith(\".d.ts\")) continue;\n\n const relativePath = relative(baseDir, fullPath);\n const route = filePathToRoute(relativePath);\n routes.push(route);\n }\n }\n}\n\nfunction filePathToRoute(filePath: string): DiscoveredRoute {\n const params: string[] = [];\n\n // 확장자 제거\n let routePath = filePath.replace(/\\.(ts|js)$/, \"\");\n\n // Windows 경로 → POSIX\n routePath = routePath.replace(/\\\\/g, \"/\");\n\n // index 파일은 디렉터리 자체\n if (routePath.endsWith(\"/index\") || routePath === \"index\") {\n routePath = routePath.replace(/\\/?index$/, \"\");\n }\n\n // [param] → :param 변환\n routePath = routePath.replace(/\\[([^\\]]+)\\]/g, (_, param) => {\n params.push(param);\n return `:${param}`;\n });\n\n // 앞에 / 추가\n const apiPath = `/${routePath}`;\n\n return {\n filePath,\n apiPath,\n params,\n };\n}\n\n/**\n * 라우트 파일에서 import하여 라우터에 등록하는 코드 생성 (빌드 타임)\n */\nexport function generateRouteImports(routes: DiscoveredRoute[], routesDir: string): string {\n const lines: string[] = [\n '// AUTO-GENERATED by Clawfire — DO NOT EDIT',\n '// This file is regenerated whenever routes change.',\n '',\n 'import { createRouter } from \"clawfire/functions\";',\n '',\n ];\n\n routes.forEach((route, i) => {\n const importPath = `./${route.filePath.replace(/\\.(ts|js)$/, \".js\")}`;\n lines.push(`import route_${i} from \"${importPath}\";`);\n });\n\n lines.push('');\n lines.push('export function registerAllRoutes(router: ReturnType<typeof createRouter>) {');\n\n routes.forEach((route, i) => {\n lines.push(` router.register(\"${route.apiPath}\", route_${i});`);\n });\n\n lines.push(' return router;');\n lines.push('}');\n\n return lines.join('\\n');\n}\n","/**\n * Clawfire Playground\n *\n * 웹 기반 API 탐색기: API 목록, 인증 테스트, 요청/응답 뷰어\n * 단일 HTML 파일로 생성되어 Firebase Hosting에 배포됩니다.\n */\n\nexport function generatePlaygroundHtml(options?: {\n title?: string;\n apiBaseUrl?: string;\n}): string {\n const title = options?.title || \"Clawfire Playground\";\n const apiBaseUrl = options?.apiBaseUrl || \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${title}</title>\n <style>\n :root {\n --bg: #0a0a0a;\n --surface: #141414;\n --surface2: #1e1e1e;\n --border: #2a2a2a;\n --text: #e5e5e5;\n --text2: #a3a3a3;\n --accent: #f97316;\n --accent2: #fb923c;\n --green: #22c55e;\n --red: #ef4444;\n --blue: #3b82f6;\n --yellow: #eab308;\n --radius: 8px;\n --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --mono: 'JetBrains Mono', 'Fira Code', monospace;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }\n\n .layout { display: grid; grid-template-columns: 320px 1fr; min-height: 100vh; }\n .sidebar { background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; }\n .main { padding: 24px; overflow-y: auto; }\n\n .logo { padding: 20px; border-bottom: 1px solid var(--border); }\n .logo h1 { font-size: 20px; font-weight: 700; color: var(--accent); }\n .logo p { font-size: 12px; color: var(--text2); margin-top: 4px; }\n\n .auth-section { padding: 16px; border-bottom: 1px solid var(--border); }\n .auth-section label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 6px; }\n .auth-input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: var(--radius); color: var(--text); font-family: var(--mono); font-size: 12px; }\n .auth-status { font-size: 11px; margin-top: 6px; }\n .auth-status.ok { color: var(--green); }\n .auth-status.no { color: var(--text2); }\n\n .search { padding: 12px 16px; border-bottom: 1px solid var(--border); }\n .search input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: var(--radius); color: var(--text); font-size: 13px; }\n\n .api-list { padding: 8px 0; }\n .api-group { padding: 4px 0; }\n .api-group-title { padding: 8px 16px; font-size: 11px; color: var(--text2); text-transform: uppercase;\n letter-spacing: 0.5px; font-weight: 600; }\n .api-item { padding: 8px 16px; cursor: pointer; transition: background 0.15s; display: flex; align-items: center;\n gap: 8px; font-size: 13px; }\n .api-item:hover { background: var(--surface2); }\n .api-item.active { background: var(--surface2); border-left: 2px solid var(--accent); }\n .api-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }\n .badge-public { background: #22c55e20; color: var(--green); }\n .badge-auth { background: #3b82f620; color: var(--blue); }\n .badge-role { background: #eab30820; color: var(--yellow); }\n .badge-reauth { background: #ef444420; color: var(--red); }\n\n .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }\n .panel-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center;\n justify-content: space-between; }\n .panel-header h2 { font-size: 16px; font-weight: 600; }\n .panel-body { padding: 16px; }\n\n .field { margin-bottom: 12px; }\n .field label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 4px; }\n .field-type { font-size: 11px; color: var(--text2); font-family: var(--mono); }\n .field-required { color: var(--red); font-size: 11px; }\n\n textarea, input[type=\"text\"] { width: 100%; padding: 10px 14px; background: var(--surface2);\n border: 1px solid var(--border); border-radius: var(--radius); color: var(--text);\n font-family: var(--mono); font-size: 13px; resize: vertical; }\n textarea { min-height: 200px; }\n\n .btn { padding: 10px 20px; border: none; border-radius: var(--radius); font-size: 14px;\n font-weight: 600; cursor: pointer; transition: all 0.15s; }\n .btn-primary { background: var(--accent); color: white; }\n .btn-primary:hover { background: var(--accent2); }\n .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }\n\n .response-section { margin-top: 16px; }\n .status-badge { font-size: 12px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }\n .status-ok { background: #22c55e20; color: var(--green); }\n .status-err { background: #ef444420; color: var(--red); }\n pre { background: var(--surface2); padding: 16px; border-radius: var(--radius); overflow-x: auto;\n font-family: var(--mono); font-size: 13px; line-height: 1.5; white-space: pre-wrap; }\n\n .schema-info { font-size: 12px; color: var(--text2); line-height: 1.6; }\n .schema-info code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; font-family: var(--mono);\n font-size: 11px; }\n\n .empty-state { text-align: center; padding: 80px 40px; color: var(--text2); }\n .empty-state h2 { font-size: 24px; margin-bottom: 8px; color: var(--text); }\n\n .timer { font-size: 12px; color: var(--text2); font-family: var(--mono); }\n\n @media (max-width: 768px) {\n .layout { grid-template-columns: 1fr; }\n .sidebar { max-height: 40vh; }\n }\n </style>\n</head>\n<body>\n <div class=\"layout\">\n <div class=\"sidebar\">\n <div class=\"logo\">\n <h1>Clawfire</h1>\n <p>API Playground</p>\n </div>\n <div class=\"auth-section\">\n <label>Bearer Token</label>\n <input type=\"text\" class=\"auth-input\" id=\"token-input\" placeholder=\"Paste your ID token...\">\n <div class=\"auth-status no\" id=\"auth-status\">Not authenticated</div>\n </div>\n <div class=\"search\">\n <input type=\"text\" id=\"search-input\" placeholder=\"Search APIs...\">\n </div>\n <div class=\"api-list\" id=\"api-list\"></div>\n </div>\n <div class=\"main\" id=\"main-content\">\n <div class=\"empty-state\">\n <h2>Select an API</h2>\n <p>Choose an API from the sidebar to test it.</p>\n </div>\n </div>\n </div>\n\n <script>\n const BASE_URL = ${JSON.stringify(apiBaseUrl)} || window.location.origin;\n let manifest = null;\n let selectedApi = null;\n\n async function loadManifest() {\n try {\n const res = await fetch(BASE_URL + '/api/__manifest', { method: 'POST' });\n manifest = await res.json();\n renderApiList(manifest.apis);\n } catch (e) {\n document.getElementById('api-list').innerHTML =\n '<div style=\"padding:16px;color:var(--red);font-size:13px;\">Failed to load API manifest. Make sure your server is running.</div>';\n }\n }\n\n function renderApiList(apis) {\n const groups = {};\n apis.forEach(api => {\n const parts = api.path.split('/').filter(Boolean);\n const group = parts.length > 1 ? parts[0] : 'root';\n if (!groups[group]) groups[group] = [];\n groups[group].push(api);\n });\n\n const el = document.getElementById('api-list');\n el.innerHTML = Object.entries(groups).map(([group, items]) =>\n '<div class=\"api-group\">' +\n '<div class=\"api-group-title\">' + group + '</div>' +\n items.map(api => {\n const auth = api.meta.auth || 'public';\n const badgeClass = 'badge-' + auth;\n return '<div class=\"api-item\" onclick=\"selectApi(\\\\'' + api.path + '\\\\')\">' +\n '<span class=\"api-badge ' + badgeClass + '\">' + auth.toUpperCase() + '</span>' +\n '<span>' + api.path + '</span>' +\n '</div>';\n }).join('') +\n '</div>'\n ).join('');\n }\n\n function selectApi(path) {\n selectedApi = manifest.apis.find(a => a.path === path);\n if (!selectedApi) return;\n\n document.querySelectorAll('.api-item').forEach(el => el.classList.remove('active'));\n event.currentTarget?.classList.add('active');\n\n const main = document.getElementById('main-content');\n const exampleInput = selectedApi.meta.exampleInput\n ? JSON.stringify(selectedApi.meta.exampleInput, null, 2)\n : generateExampleFromSchema(selectedApi.inputSchema);\n\n main.innerHTML =\n '<div class=\"panel\">' +\n '<div class=\"panel-header\">' +\n '<h2>POST ' + selectedApi.path + '</h2>' +\n '<span class=\"api-badge badge-' + (selectedApi.meta.auth || 'public') + '\">' +\n (selectedApi.meta.auth || 'public').toUpperCase() + '</span>' +\n '</div>' +\n '<div class=\"panel-body\">' +\n '<p style=\"color:var(--text2);margin-bottom:16px;\">' + (selectedApi.meta.description || '') + '</p>' +\n (selectedApi.meta.tags ? '<div style=\"margin-bottom:12px;\">' + selectedApi.meta.tags.map(t =>\n '<span style=\"background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:11px;margin-right:4px;\">' + t + '</span>'\n ).join('') + '</div>' : '') +\n '<div class=\"schema-info\" style=\"margin-bottom:16px;\">' +\n '<strong>Input Schema:</strong><br>' + renderSchemaInfo(selectedApi.inputSchema) +\n '</div>' +\n '<div class=\"schema-info\" style=\"margin-bottom:16px;\">' +\n '<strong>Output Schema:</strong><br>' + renderSchemaInfo(selectedApi.outputSchema) +\n '</div>' +\n '</div>' +\n '</div>' +\n '<div class=\"panel\">' +\n '<div class=\"panel-header\"><h2>Request</h2></div>' +\n '<div class=\"panel-body\">' +\n '<textarea id=\"req-body\" placeholder=\"Request JSON body\">' + exampleInput + '</textarea>' +\n '<div style=\"margin-top:12px;display:flex;align-items:center;gap:12px;\">' +\n '<button class=\"btn btn-primary\" onclick=\"sendRequest()\">Send Request</button>' +\n '<span class=\"timer\" id=\"timer\"></span>' +\n '</div>' +\n '</div>' +\n '</div>' +\n '<div class=\"response-section\" id=\"response-section\"></div>';\n }\n\n function renderSchemaInfo(schema) {\n if (!schema || !schema.properties) return '<code>void</code>';\n return Object.entries(schema.properties).map(([key, prop]) => {\n const required = schema.required?.includes(key);\n const type = prop.type || 'unknown';\n const enumVals = prop.enum ? ' (' + prop.enum.join(', ') + ')' : '';\n return '<code>' + key + '</code>: <span class=\"field-type\">' + type + enumVals + '</span>' +\n (required ? ' <span class=\"field-required\">required</span>' : ' <span style=\"color:var(--text2);font-size:11px;\">optional</span>');\n }).join('<br>');\n }\n\n function generateExampleFromSchema(schema) {\n if (!schema || !schema.properties) return '{}';\n const obj = {};\n for (const [key, prop] of Object.entries(schema.properties)) {\n if (prop.enum) { obj[key] = prop.enum[0]; continue; }\n switch (prop.type) {\n case 'string': obj[key] = prop.format === 'email' ? 'user@example.com' : 'string'; break;\n case 'number': obj[key] = 0; break;\n case 'boolean': obj[key] = false; break;\n case 'array': obj[key] = []; break;\n case 'object': obj[key] = {}; break;\n default: obj[key] = null;\n }\n }\n return JSON.stringify(obj, null, 2);\n }\n\n async function sendRequest() {\n if (!selectedApi) return;\n const body = document.getElementById('req-body').value;\n const token = document.getElementById('token-input').value;\n const timer = document.getElementById('timer');\n const section = document.getElementById('response-section');\n\n let parsed;\n try { parsed = JSON.parse(body); } catch {\n section.innerHTML = '<div class=\"panel\"><div class=\"panel-body\"><pre style=\"color:var(--red)\">Invalid JSON</pre></div></div>';\n return;\n }\n\n const start = performance.now();\n timer.textContent = 'Sending...';\n\n try {\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers['Authorization'] = 'Bearer ' + token;\n\n const res = await fetch(BASE_URL + '/api' + selectedApi.path, {\n method: 'POST', headers, body: JSON.stringify(parsed)\n });\n const elapsed = Math.round(performance.now() - start);\n timer.textContent = elapsed + 'ms';\n\n const json = await res.json();\n const isOk = res.ok;\n\n section.innerHTML =\n '<div class=\"panel\">' +\n '<div class=\"panel-header\">' +\n '<h2>Response</h2>' +\n '<span class=\"status-badge ' + (isOk ? 'status-ok' : 'status-err') + '\">' +\n res.status + ' ' + res.statusText + '</span>' +\n '</div>' +\n '<div class=\"panel-body\"><pre>' + syntaxHighlight(JSON.stringify(json, null, 2)) + '</pre></div>' +\n '</div>';\n } catch (e) {\n const elapsed = Math.round(performance.now() - start);\n timer.textContent = elapsed + 'ms';\n section.innerHTML =\n '<div class=\"panel\"><div class=\"panel-body\"><pre style=\"color:var(--red)\">Network error: ' + e.message + '</pre></div></div>';\n }\n }\n\n function syntaxHighlight(json) {\n return json.replace(/(\"(\\\\\\\\u[a-fA-F0-9]{4}|\\\\\\\\[^u]|[^\\\\\\\\\"])*\"(\\\\s*:)?)|\\\\b(true|false|null)\\\\b|-?\\\\d+(\\\\.\\\\d+)?([eE][+-]?\\\\d+)?/g,\n function(match) {\n let cls = 'color:#eab308';\n if (/^\"/.test(match)) {\n if (/:$/.test(match)) cls = 'color:#3b82f6';\n else cls = 'color:#22c55e';\n } else if (/true|false/.test(match)) cls = 'color:#f97316';\n else if (/null/.test(match)) cls = 'color:#ef4444';\n return '<span style=\"' + cls + '\">' + match + '</span>';\n }\n );\n }\n\n // Search\n document.getElementById('search-input')?.addEventListener('input', (e) => {\n if (!manifest) return;\n const q = e.target.value.toLowerCase();\n const filtered = manifest.apis.filter(a => a.path.toLowerCase().includes(q) || a.meta.description?.toLowerCase().includes(q));\n renderApiList(filtered);\n });\n\n // Token status\n document.getElementById('token-input')?.addEventListener('input', (e) => {\n const el = document.getElementById('auth-status');\n if (e.target.value) {\n el.textContent = 'Token set';\n el.className = 'auth-status ok';\n } else {\n el.textContent = 'Not authenticated';\n el.className = 'auth-status no';\n }\n });\n\n loadManifest();\n </script>\n</body>\n</html>`;\n}\n","/**\n * Clawfire File Watcher\n *\n * Node.js fs.watch 기반 파일 변경 감지 (외부 의존성 없음)\n * macOS/Windows에서 recursive 지원, Linux에서는 디렉터리별 개별 감시\n */\nimport { watch, existsSync, statSync, readdirSync } from \"fs\";\nimport { join, extname } from \"path\";\nimport { EventEmitter } from \"events\";\n\nexport type WatchEventType = \"route-change\" | \"schema-change\" | \"config-change\" | \"any-change\";\n\nexport interface WatchEvent {\n type: WatchEventType;\n filePath: string;\n timestamp: number;\n}\n\nexport class FileWatcher extends EventEmitter {\n private watchers: ReturnType<typeof watch>[] = [];\n private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n private debounceMs: number;\n\n constructor(debounceMs = 150) {\n super();\n this.debounceMs = debounceMs;\n }\n\n /**\n * 디렉터리 감시 시작\n */\n watchDir(dir: string, eventType: WatchEventType): this {\n if (!existsSync(dir)) return this;\n\n try {\n // macOS/Windows: recursive 옵션 지원\n const watcher = watch(dir, { recursive: true }, (event, filename) => {\n if (!filename) return;\n const filePath = join(dir, filename);\n if (!this.isWatchedFile(filePath)) return;\n this.emitDebounced(filePath, eventType);\n });\n\n watcher.on(\"error\", (err) => {\n // Linux에서 recursive 미지원 시 fallback\n if ((err as any).code === \"ERR_FEATURE_UNAVAILABLE_ON_PLATFORM\") {\n this.watchDirRecursiveManual(dir, eventType);\n }\n });\n\n this.watchers.push(watcher);\n } catch {\n // fallback: 수동 재귀 감시\n this.watchDirRecursiveManual(dir, eventType);\n }\n\n return this;\n }\n\n /**\n * 단일 파일 감시\n */\n watchFile(filePath: string, eventType: WatchEventType): this {\n if (!existsSync(filePath)) return this;\n\n const watcher = watch(filePath, (event) => {\n this.emitDebounced(filePath, eventType);\n });\n\n this.watchers.push(watcher);\n return this;\n }\n\n /**\n * Linux fallback: 디렉터리 수동 재귀 감시\n */\n private watchDirRecursiveManual(dir: string, eventType: WatchEventType): void {\n if (!existsSync(dir)) return;\n\n const watcher = watch(dir, (event, filename) => {\n if (!filename) return;\n const filePath = join(dir, filename);\n if (!this.isWatchedFile(filePath)) return;\n this.emitDebounced(filePath, eventType);\n });\n this.watchers.push(watcher);\n\n // 서브디렉터리도 감시\n try {\n const entries = readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith(\".\") && entry.name !== \"node_modules\") {\n this.watchDirRecursiveManual(join(dir, entry.name), eventType);\n }\n }\n } catch {}\n }\n\n /**\n * 감시 대상 파일인지 확인\n */\n private isWatchedFile(filePath: string): boolean {\n const ext = extname(filePath);\n return [\".ts\", \".tsx\", \".js\", \".jsx\", \".json\"].includes(ext);\n }\n\n /**\n * 디바운스 이벤트 발생\n */\n private emitDebounced(filePath: string, type: WatchEventType): void {\n const existing = this.debounceTimers.get(filePath);\n if (existing) clearTimeout(existing);\n\n this.debounceTimers.set(\n filePath,\n setTimeout(() => {\n this.debounceTimers.delete(filePath);\n const event: WatchEvent = { type, filePath, timestamp: Date.now() };\n this.emit(\"change\", event);\n this.emit(type, event);\n }, this.debounceMs),\n );\n }\n\n /**\n * 모든 감시 중지\n */\n close(): void {\n for (const watcher of this.watchers) {\n watcher.close();\n }\n this.watchers = [];\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n this.removeAllListeners();\n }\n}\n"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBA8HO,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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","names":["resolve","relative","existsSync","existsSync","readdirSync","join","resolve","existsSync","relative"]}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ discoverRoutes,
4
+ generateRouteImports
5
+ } from "./chunk-37Y2XI7X.js";
6
+ export {
7
+ discoverRoutes,
8
+ generateRouteImports
9
+ };
@@ -0,0 +1,28 @@
1
+ interface DiscoveredRoute {
2
+ /** 파일 경로 (상대) */
3
+ filePath: string;
4
+ /** API 경로 (/products/list) */
5
+ apiPath: string;
6
+ /** 동적 파라미터 이름 */
7
+ params: string[];
8
+ }
9
+ /**
10
+ * routes 디렉터리에서 라우트 파일 자동 발견
11
+ *
12
+ * @param routesDir - routes 디렉터리 절대 경로
13
+ * @returns 발견된 라우트 목록
14
+ *
15
+ * @example
16
+ * ```
17
+ * app/routes/products/list.ts → { apiPath: "/products/list", params: [] }
18
+ * app/routes/products/[id]/get.ts → { apiPath: "/products/:id/get", params: ["id"] }
19
+ * app/routes/health.ts → { apiPath: "/health", params: [] }
20
+ * ```
21
+ */
22
+ declare function discoverRoutes(routesDir: string): DiscoveredRoute[];
23
+ /**
24
+ * 라우트 파일에서 import하여 라우터에 등록하는 코드 생성 (빌드 타임)
25
+ */
26
+ declare function generateRouteImports(routes: DiscoveredRoute[], routesDir: string): string;
27
+
28
+ export { type DiscoveredRoute as D, discoverRoutes as d, generateRouteImports as g };
@@ -0,0 +1,28 @@
1
+ interface DiscoveredRoute {
2
+ /** 파일 경로 (상대) */
3
+ filePath: string;
4
+ /** API 경로 (/products/list) */
5
+ apiPath: string;
6
+ /** 동적 파라미터 이름 */
7
+ params: string[];
8
+ }
9
+ /**
10
+ * routes 디렉터리에서 라우트 파일 자동 발견
11
+ *
12
+ * @param routesDir - routes 디렉터리 절대 경로
13
+ * @returns 발견된 라우트 목록
14
+ *
15
+ * @example
16
+ * ```
17
+ * app/routes/products/list.ts → { apiPath: "/products/list", params: [] }
18
+ * app/routes/products/[id]/get.ts → { apiPath: "/products/:id/get", params: ["id"] }
19
+ * app/routes/health.ts → { apiPath: "/health", params: [] }
20
+ * ```
21
+ */
22
+ declare function discoverRoutes(routesDir: string): DiscoveredRoute[];
23
+ /**
24
+ * 라우트 파일에서 import하여 라우터에 등록하는 코드 생성 (빌드 타임)
25
+ */
26
+ declare function generateRouteImports(routes: DiscoveredRoute[], routesDir: string): string;
27
+
28
+ export { type DiscoveredRoute as D, discoverRoutes as d, generateRouteImports as g };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Clawfire Error System
3
+ *
4
+ * 표준화된 에러 코드와 구조로 일관된 에러 응답을 제공합니다.
5
+ */
6
+ type ClawfireErrorCode = "VALIDATION_ERROR" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "REAUTH_REQUIRED" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE";
7
+ declare class ClawfireError extends Error {
8
+ readonly code: ClawfireErrorCode;
9
+ readonly statusCode: number;
10
+ readonly details?: unknown;
11
+ constructor(code: ClawfireErrorCode, message: string, details?: unknown);
12
+ toJSON(): {
13
+ error: {
14
+ details?: {} | undefined;
15
+ code: ClawfireErrorCode;
16
+ message: string;
17
+ };
18
+ };
19
+ }
20
+ /** 편의 팩토리 함수 */
21
+ declare const Errors: {
22
+ validation: (message: string, details?: unknown) => ClawfireError;
23
+ unauthorized: (message?: string) => ClawfireError;
24
+ forbidden: (message?: string) => ClawfireError;
25
+ notFound: (message?: string) => ClawfireError;
26
+ conflict: (message: string) => ClawfireError;
27
+ rateLimited: (message?: string) => ClawfireError;
28
+ reauthRequired: (message?: string) => ClawfireError;
29
+ internal: (message?: string) => ClawfireError;
30
+ unavailable: (message?: string) => ClawfireError;
31
+ };
32
+
33
+ export { ClawfireError as C, Errors as E, type ClawfireErrorCode as a };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Clawfire Error System
3
+ *
4
+ * 표준화된 에러 코드와 구조로 일관된 에러 응답을 제공합니다.
5
+ */
6
+ type ClawfireErrorCode = "VALIDATION_ERROR" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "REAUTH_REQUIRED" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE";
7
+ declare class ClawfireError extends Error {
8
+ readonly code: ClawfireErrorCode;
9
+ readonly statusCode: number;
10
+ readonly details?: unknown;
11
+ constructor(code: ClawfireErrorCode, message: string, details?: unknown);
12
+ toJSON(): {
13
+ error: {
14
+ details?: {} | undefined;
15
+ code: ClawfireErrorCode;
16
+ message: string;
17
+ };
18
+ };
19
+ }
20
+ /** 편의 팩토리 함수 */
21
+ declare const Errors: {
22
+ validation: (message: string, details?: unknown) => ClawfireError;
23
+ unauthorized: (message?: string) => ClawfireError;
24
+ forbidden: (message?: string) => ClawfireError;
25
+ notFound: (message?: string) => ClawfireError;
26
+ conflict: (message: string) => ClawfireError;
27
+ rateLimited: (message?: string) => ClawfireError;
28
+ reauthRequired: (message?: string) => ClawfireError;
29
+ internal: (message?: string) => ClawfireError;
30
+ unavailable: (message?: string) => ClawfireError;
31
+ };
32
+
33
+ export { ClawfireError as C, Errors as E, type ClawfireErrorCode as a };