mega-framework 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 (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. package/vitest.config.js +33 -0
@@ -0,0 +1,388 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Router — 라우트 파일이 사용하는 등록 인터페이스.
5
+ *
6
+ * 라우트 파일 형식:
7
+ * export default (router) => {
8
+ * router.http.get('/api/users', handler, { before: [auth] })
9
+ * router.http.post('/api/users', UsersController.create, { schema, after: [logResponse] })
10
+ * router.ws('/ws/chat', ChatChannel, { before: [auth] })
11
+ * }
12
+ *
13
+ * 핸들러는 inline async function 또는 static method reference (ADR-074).
14
+ * `typeof handler !== 'function'` → 부팅 시 throw (ADR-089).
15
+ *
16
+ * 미들웨어 라이프사이클 (ADR-091):
17
+ * - before: preHandler 단계 (인증·rateLimit·검증)
18
+ * - transform: preSerialization 이른 단계 (raw data 변환, envelope wrap 전)
19
+ * - after: onResponse 단계 (로깅·메트릭, side-effect만, throw → warn 로그)
20
+ */
21
+
22
+ // 명명 import — AJV 8 은 ESM/TS 정합을 위해 `Ajv` named export 를 제공한다(default 는 TS 에서
23
+ // construct 불가로 잡힘). `ValidateFunction` 등 타입도 같은 모듈에서 가져온다.
24
+ import { Ajv } from 'ajv'
25
+ import { MegaError } from '../errors/mega-error.js'
26
+ import { MegaWebSocketController } from './ws-controller.js'
27
+ import { getHttpCtx } from './ctx-builder.js'
28
+
29
+ const HTTP_METHODS = Object.freeze(['get', 'post', 'put', 'patch', 'delete', 'head', 'options'])
30
+
31
+ /**
32
+ * WS payload 스키마 검증용 공유 AJV 인스턴스 (M2, ADR-096).
33
+ * `allErrors: true` — 위반을 모두 모아 envelope details 배열(ADR-075)로 돌려준다.
34
+ * HTTP 검증(Fastify 네이티브 AJV)과 별개 인스턴스다 — WS 는 Fastify route 가 아니라
35
+ * 우리 dispatch 경로에서 payload 만 검증하므로 직접 컴파일·실행한다.
36
+ */
37
+ const wsAjv = new Ajv({ allErrors: true })
38
+
39
+ /**
40
+ * `router.ws({ schemas })` 의 type 별 JSON Schema 를 **등록(부팅) 시점에 사전 컴파일**한다
41
+ * (ADR-089 fail-fast). 형식 위반·컴파일 실패는 즉시 throw 해 잘못된 스키마로 부팅되지 않게 한다.
42
+ *
43
+ * @param {unknown} schemas - `{ [messageType: string]: JSONSchema }` 또는 undefined.
44
+ * @param {string} path - 에러 메시지용 WS 경로.
45
+ * @returns {Record<string, import('ajv').ValidateFunction> | null}
46
+ * 컴파일된 검증 함수 맵. schemas 미지정 시 null (검증 없음).
47
+ * @throws {MegaRouteError} schemas 가 object 아님 / 항목이 schema object 아님 / 컴파일 실패.
48
+ */
49
+ function compileWsSchemas(schemas, path) {
50
+ if (schemas === undefined) return null
51
+ if (schemas === null || typeof schemas !== 'object' || Array.isArray(schemas)) {
52
+ throw new MegaRouteError(
53
+ 'route.ws_invalid_schemas',
54
+ `router.ws('${path}'): 'schemas' must be an object mapping message type → JSON Schema. ` +
55
+ `Got: ${Array.isArray(schemas) ? 'array' : schemas === null ? 'null' : typeof schemas}.`,
56
+ )
57
+ }
58
+ /** @type {Record<string, import('ajv').ValidateFunction>} */
59
+ const validators = {}
60
+ for (const [type, schema] of Object.entries(schemas)) {
61
+ if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
62
+ throw new MegaRouteError(
63
+ 'route.ws_invalid_schema_entry',
64
+ `router.ws('${path}'): schemas['${type}'] must be a JSON Schema object. ` +
65
+ `Got: ${Array.isArray(schema) ? 'array' : schema === null ? 'null' : typeof schema}.`,
66
+ )
67
+ }
68
+ try {
69
+ validators[type] = wsAjv.compile(schema)
70
+ } catch (err) {
71
+ // AJV 컴파일 실패 = 잘못된 스키마 정의. 부팅 시 fail-fast (우회 금지, 원인 보존).
72
+ throw new MegaRouteError(
73
+ 'route.ws_schema_compile_failed',
74
+ `router.ws('${path}'): failed to compile JSON Schema for type '${type}': ` +
75
+ `${err instanceof Error ? err.message : String(err)}`,
76
+ { cause: err },
77
+ )
78
+ }
79
+ }
80
+ return validators
81
+ }
82
+
83
+ /**
84
+ * 라우트 등록 옵션 (router.http.*(path, handler, opts) / router.ws(path, Channel, opts)).
85
+ * @typedef {object} HttpRouteOpts
86
+ * @property {Function[]} [before] - preHandler 단계 미들웨어 (인증·rateLimit·검증).
87
+ * @property {Function[]} [transform] - preSerialization 이른 단계 변환 (HTTP 전용).
88
+ * @property {Function[]} [after] - onResponse 단계 side-effect (HTTP 전용).
89
+ * @property {Object} [schema] - Fastify schema (ADR-019).
90
+ * @property {{ tags?: string[], summary?: string, description?: string, deprecated?: boolean }} [openapi] -
91
+ * OpenAPI 라우트 메타 (ADR-070/140). 라우트 schema 의 비검증 메타 필드로 병합돼 명세에 반영(openapi 옵트인 시).
92
+ */
93
+
94
+ /** 라우트 등록 시 잘못된 형식 시 throw. */
95
+ export class MegaRouteError extends MegaError {
96
+ // code = 'route.<reason>'
97
+ }
98
+
99
+ /**
100
+ * Router 인스턴스 — MegaApp 부팅 시 각 라우트 파일마다 생성·전달.
101
+ *
102
+ * HTTP 핸들러 등록 + before/transform/after 미들웨어 wiring.
103
+ * WS 는 hub 통합 시 등록 시그니처만 보존, 실제 핸들러는 inline /
104
+ * Static method ref 패턴 지원.
105
+ */
106
+ export class Router {
107
+ /**
108
+ * @param {Object} ctx
109
+ * @param {import('fastify').FastifyInstance} ctx.fastify - 등록 대상 Fastify
110
+ * @param {string} ctx.appName - 디버그·에러 메시지용 앱 이름
111
+ * @param {string} [ctx.sourceFile] - 라우트 파일 경로 (에러 메시지용)
112
+ * @param {import('./mega-app.js').MegaApp} [ctx.app] - 바인딩 앱. HTTP 핸들러 ctx 의 `db/cache/bus`
113
+ * 접근자(ADR-102) 출처. 미지정(standalone Router)이면 ctx 에 어댑터 접근자가 빠진다.
114
+ */
115
+ constructor(ctx) {
116
+ if (!ctx?.fastify) throw new MegaRouteError('route.invalid_ctx', 'Router: fastify is required')
117
+ this._fastify = ctx.fastify
118
+ this._appName = ctx.appName
119
+ this._sourceFile = ctx.sourceFile
120
+ /** @type {import('./mega-app.js').MegaApp | null} */
121
+ this._app = ctx.app ?? null
122
+ this.http = this._buildHttpProxy()
123
+ }
124
+
125
+ /**
126
+ * 바인딩된 MegaApp (standalone Router 면 null).
127
+ *
128
+ * 라우트 모듈(`export default (router) => {}`)이 앱 표면에 닿는 통로다 — 특히 WS `before(req)`
129
+ * upgrade 인증은 Fastify 밖이라 `req.session` 이 없어, `router.app.sessionStore` 로 세션 스토어를
130
+ * 얻어 {@link import('./session.js').readSession} 으로 신원을 확인한다(ADR-159).
131
+ *
132
+ * @returns {import('./mega-app.js').MegaApp | null}
133
+ */
134
+ get app() {
135
+ return this._app
136
+ }
137
+
138
+ /**
139
+ * HTTP 프록시: router.http.get(...), router.http.post(...) 등 7개 메서드.
140
+ * @private
141
+ */
142
+ _buildHttpProxy() {
143
+ /** @type {Record<string, (path: string, handler: Function, opts?: HttpRouteOpts) => void>} */
144
+ const proxy = {}
145
+ for (const method of HTTP_METHODS) {
146
+ proxy[method] = (path, handler, opts = {}) => this._registerHttp(method, path, handler, opts)
147
+ }
148
+ return Object.freeze(proxy)
149
+ }
150
+
151
+ /**
152
+ * 실제 HTTP 라우트 등록 — Fastify route() + before/transform/after wiring.
153
+ * @param {string} method - HTTP 메서드 (소문자).
154
+ * @param {string} path - 라우트 경로.
155
+ * @param {Function} handler - 핸들러 (inline async fn 또는 static method ref).
156
+ * @param {HttpRouteOpts} opts
157
+ * @private
158
+ */
159
+ _registerHttp(method, path, handler, opts) {
160
+ if (typeof path !== 'string' || path.length === 0) {
161
+ throw new MegaRouteError(
162
+ 'route.invalid_path',
163
+ `router.http.${method}: path must be a non-empty string. Got: ${typeof path}`,
164
+ )
165
+ }
166
+ if (typeof handler !== 'function') {
167
+ throw new MegaRouteError(
168
+ 'route.invalid_handler',
169
+ `router.http.${method}('${path}'): handler must be a function ` +
170
+ `(inline async fn or static method reference like UsersController.index). ` +
171
+ `Got: ${typeof handler}.`,
172
+ )
173
+ }
174
+
175
+ const before = this._validateMiddlewareArray(opts.before, 'before', method, path)
176
+ const transform = this._validateMiddlewareArray(opts.transform, 'transform', method, path)
177
+ const after = this._validateMiddlewareArray(opts.after, 'after', method, path)
178
+
179
+ // 동적으로 schema/preHandler/preSerialization/onResponse 를 덧붙이므로 permissive 타입.
180
+ /** @type {Record<string, any>} */
181
+ const routeOpts = {
182
+ method: method.toUpperCase(),
183
+ url: path,
184
+ handler: async (
185
+ /** @type {import('fastify').FastifyRequest} */ req,
186
+ /** @type {import('fastify').FastifyReply} */ reply,
187
+ ) => {
188
+ // canonical 핸들러 시그니처 (req, res, ctx) (ADR-074, docs/03 §581). ctx 는 요청 단위로 만들어
189
+ // app/log/requestId/req/reply + db/cache/bus 접근자(ADR-102)를 노출. 기존 (req, reply) 핸들러는
190
+ // 3번째 인자를 무시하므로 하위 호환. getHttpCtx 는 요청당 1회 캐싱이라 글로벌 미들웨어가 먼저
191
+ // 만든 ctx 를 그대로 이어받는다(ADR-134 — 미들웨어→핸들러 ctx 공유).
192
+ const ctx = getHttpCtx({ app: this._app, req, reply })
193
+ return handler(req, reply, ctx)
194
+ },
195
+ }
196
+
197
+ // schema (ADR-019) 그대로 Fastify 에 전달
198
+ if (opts.schema) routeOpts.schema = opts.schema
199
+
200
+ // OpenAPI 메타(ADR-070/140) — 라우트 옵션 openapi:{tags,summary,description,deprecated} 를 Fastify
201
+ // 라우트 schema 의 비검증 메타 필드로 병합한다(@fastify/swagger 가 schema 에서 수집). 검증 필드(body 등)는
202
+ // 그대로 두고 메타만 얹으므로 검증 동작에 영향 없음. openapi 옵트인 미등록이어도 무해(수집기가 없을 뿐).
203
+ if (opts.openapi && typeof opts.openapi === 'object') {
204
+ const meta = /** @type {Record<string, any>} */ (opts.openapi)
205
+ routeOpts.schema = { ...(routeOpts.schema || {}) }
206
+ if (Array.isArray(meta.tags)) routeOpts.schema.tags = meta.tags
207
+ if (typeof meta.summary === 'string') routeOpts.schema.summary = meta.summary
208
+ if (typeof meta.description === 'string') routeOpts.schema.description = meta.description
209
+ if (meta.deprecated === true) routeOpts.schema.deprecated = true
210
+ }
211
+
212
+ // before — Fastify preHandler 체인. 각 미들웨어를 arity-2 async 래퍼로 감싼다.
213
+ //
214
+ // 왜 래핑하나: Fastify 는 async preHandler 의 arity 가 3 이상이면 3번째 인자를 콜백 `done` 으로
215
+ // 오해해 "Async function has too many arguments" 로 **등록을 거부**한다. 그런데 인증 가드
216
+ // `requireAuth`/`requireRole`(ADR-143)은 시그니처가 `(req, reply, ctx)` 로 arity 3 다 — 글로벌
217
+ // 미들웨어 경로(ADR-134)에서 ctx 를 받기 위함. 라우트 `before` 는 ctx 를 주입하지 않으므로(=undefined,
218
+ // 가드는 req.session 으로 동작) arity-2 래퍼 `(req, reply)` 로 감싸면 Fastify 계약에 맞으면서 ADR-143 이
219
+ // 문서화한 `{ before: [requireAuth] }` 가 실제로 동작한다. 래퍼는 각각 독립 preHandler 라 순서·reply
220
+ // 단락(앞 미들웨어가 응답 전송 시 이후 미들웨어·핸들러 skip) 의미는 그대로다(ADR-156).
221
+ if (before.length > 0) {
222
+ routeOpts.preHandler = before.map(
223
+ (fn) =>
224
+ /** @param {import('fastify').FastifyRequest} req @param {import('fastify').FastifyReply} reply */
225
+ async (req, reply) => fn(req, reply),
226
+ )
227
+ }
228
+
229
+ // transform — preSerialization 이른 단계. Fastify preSerialization 훅의 payload 를 변환.
230
+ if (transform.length > 0) {
231
+ routeOpts.preSerialization = async (
232
+ /** @type {import('fastify').FastifyRequest} */ req,
233
+ /** @type {import('fastify').FastifyReply} */ reply,
234
+ /** @type {any} */ payload,
235
+ ) => {
236
+ let current = payload
237
+ for (const fn of transform) {
238
+ current = await fn(req, reply, current)
239
+ }
240
+ return current
241
+ }
242
+ }
243
+
244
+ // after — onResponse 단계 (응답 전송 후). throw 시 warn 로그 + 응답 영향 없음 (ADR-091).
245
+ if (after.length > 0) {
246
+ routeOpts.onResponse = async (
247
+ /** @type {import('fastify').FastifyRequest} */ req,
248
+ /** @type {import('fastify').FastifyReply} */ reply,
249
+ ) => {
250
+ for (const fn of after) {
251
+ try {
252
+ await fn(req, reply)
253
+ } catch (err) {
254
+ // ADR-091: silent fallback 금지 — warn 로그.
255
+ const log = req.log ?? console
256
+ log.warn?.({ err, hook: 'after', method, path }, `after middleware threw — ignored (response already sent)`)
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ // routeOpts 는 동적 조립(method 대문자화·옵션 키 추가)이라 RouteOptions 로 캐스팅해 전달.
263
+ this._fastify.route(/** @type {import('fastify').RouteOptions} */ (routeOpts))
264
+ }
265
+
266
+ /**
267
+ * WebSocket 라우트 등록 (실 upgrade wiring).
268
+ *
269
+ * `ChannelClass` 가 {@link MegaWebSocketController} 를 상속하는지 **부팅 시 검증** (ADR-089 fail-fast).
270
+ * 등록된 라우트는 `fastify._megaWsRoutes` 에 보관되고, MegaApp 이 listen 시점에 HTTP `'upgrade'`
271
+ * 이벤트에 연결한다 (core/ws-upgrade.js).
272
+ *
273
+ * WS 는 `before`(upgrade 인증) 만 지원한다 — `transform`/`after` 는 HTTP 전용 (ADR-074/091).
274
+ *
275
+ * `schemas` 는 **메시지 type 별 payload JSON Schema** 맵이다 (`{ [type]: jsonSchema }`, M2).
276
+ * 등록 시점에 AJV 로 사전 컴파일되며, 수신 메시지의 `type` 에 해당 스키마가 있으면 dispatch
277
+ * 직전에 `payload` 를 검증한다. 실패 시 `ws.invalid_payload` error envelope 응답(연결 유지) —
278
+ * HTTP AJV 매핑(ADR-090)과 동일한 fail-closed 정책. 스키마 없는 type 은 그대로 통과.
279
+ *
280
+ * @param {string} path - WS 경로 (= ASP namespace, 예: '/ws/chat').
281
+ * @param {Function} ChannelClass - MegaWebSocketController 를 상속한 채널 클래스.
282
+ * @param {{ before?: Function[], schemas?: Object, transform?: unknown, after?: unknown }} [opts]
283
+ * - schemas: `{ [messageType]: JSONSchema }` — type 별 payload 스키마 (AJV 사전 컴파일됨).
284
+ * @throws {MegaRouteError} path/ChannelClass 형식 위반, 상속 위반, 또는 schemas 형식·컴파일 실패.
285
+ */
286
+ ws(path, ChannelClass, opts = {}) {
287
+ if (typeof path !== 'string' || path.length === 0) {
288
+ throw new MegaRouteError(
289
+ 'route.invalid_path',
290
+ `router.ws: path must be a non-empty string. Got: ${typeof path}`,
291
+ )
292
+ }
293
+ if (typeof ChannelClass !== 'function') {
294
+ throw new MegaRouteError(
295
+ 'route.ws_channel_class_required',
296
+ `router.ws('${path}'): ChannelClass must be a class extending MegaWebSocketController. ` +
297
+ `Got: ${typeof ChannelClass}.`,
298
+ )
299
+ }
300
+ // 상속 검증 (ADR-089 fail-fast) — dispatch/_bind/라이프사이클 훅이 보장돼야 upgrade 가 동작.
301
+ if (
302
+ ChannelClass !== MegaWebSocketController &&
303
+ !(ChannelClass.prototype instanceof MegaWebSocketController)
304
+ ) {
305
+ throw new MegaRouteError(
306
+ 'route.ws_channel_not_controller',
307
+ `router.ws('${path}'): ChannelClass must extend MegaWebSocketController ` +
308
+ `(import { MegaWebSocketController } from 'mega-framework'). Got: ${ChannelClass.name || 'anonymous'}.`,
309
+ )
310
+ }
311
+ // before 만 WS 에 의미 있음 (upgrade 인증). transform/after 는 HTTP 전용.
312
+ this._validateMiddlewareArray(opts.before, 'before', 'ws', path)
313
+ if (opts.transform !== undefined) {
314
+ throw new MegaRouteError(
315
+ 'route.ws_no_transform',
316
+ `router.ws('${path}'): 'transform' is HTTP-only. WS messages dispatch by type inside the channel (ADR-074).`,
317
+ )
318
+ }
319
+ if (opts.after !== undefined) {
320
+ throw new MegaRouteError(
321
+ 'route.ws_no_after',
322
+ `router.ws('${path}'): 'after' is HTTP-only. WS messages dispatch by type inside the channel (ADR-074).`,
323
+ )
324
+ }
325
+
326
+ // schemas (M2) — type 별 payload JSON Schema 사전 컴파일. 형식·컴파일 실패는 여기서 throw.
327
+ const schemaValidators = compileWsSchemas(opts.schemas, path)
328
+
329
+ // ns = path (코어 단위는 namespace=채널, ADR-034). MegaApp 이 ASP 활성 여부 판단에 사용.
330
+ // _megaWsRoutes 는 Fastify 타입에 없는 우리 전용 보관소 — 부착 시 cast.
331
+ const fastifyWithWs =
332
+ /** @type {import('fastify').FastifyInstance & { _megaWsRoutes?: Array<{ path: string, ns: string, ChannelClass: Function, opts: Object, schemaValidators: Record<string, import('ajv').ValidateFunction> | null }> }} */ (
333
+ this._fastify
334
+ )
335
+ if (!fastifyWithWs._megaWsRoutes) fastifyWithWs._megaWsRoutes = []
336
+ const existing = fastifyWithWs._megaWsRoutes.find((r) => r.path === path)
337
+ if (existing) {
338
+ throw new MegaRouteError(
339
+ 'route.ws_duplicate_path',
340
+ `router.ws('${path}'): a WS route is already registered for this path.`,
341
+ )
342
+ }
343
+ fastifyWithWs._megaWsRoutes.push({ path, ns: path, ChannelClass, opts, schemaValidators })
344
+ }
345
+
346
+ /**
347
+ * 파일 전체 적용 미들웨어 (router.use). fastify.addHook 으로 등록.
348
+ * 실행 순서: 전역 → 앱 → 파일(router.use) → 라우트(before opts) → handler.
349
+ * @param {Function} middleware
350
+ */
351
+ use(middleware) {
352
+ if (typeof middleware !== 'function') {
353
+ throw new MegaRouteError(
354
+ 'route.invalid_use',
355
+ `router.use: middleware must be a function. Got: ${typeof middleware}.`,
356
+ )
357
+ }
358
+ // 사용자 제공 범용 미들웨어 — Fastify preHandler 훅 시그니처로 캐스팅.
359
+ this._fastify.addHook('preHandler', /** @type {any} */ (middleware))
360
+ }
361
+
362
+ /**
363
+ * @param {unknown} arr - 검증 대상 (Function[] 기대).
364
+ * @param {string} name - 옵션 이름 (before/transform/after).
365
+ * @param {string} method - HTTP 메서드 또는 'ws'.
366
+ * @param {string} path - 라우트 경로.
367
+ * @returns {Function[]}
368
+ * @private
369
+ */
370
+ _validateMiddlewareArray(arr, name, method, path) {
371
+ if (arr === undefined) return []
372
+ if (!Array.isArray(arr)) {
373
+ throw new MegaRouteError(
374
+ `route.invalid_${name}`,
375
+ `router.${method === 'ws' ? 'ws' : `http.${method}`}('${path}'): '${name}' must be an array of functions. Got: ${typeof arr}.`,
376
+ )
377
+ }
378
+ arr.forEach((fn, i) => {
379
+ if (typeof fn !== 'function') {
380
+ throw new MegaRouteError(
381
+ `route.invalid_${name}_item`,
382
+ `router.${method === 'ws' ? 'ws' : `http.${method}`}('${path}'): '${name}[${i}]' must be a function. Got: ${typeof fn}.`,
383
+ )
384
+ }
385
+ })
386
+ return arr
387
+ }
388
+ }
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+ import { readdirSync, statSync } from 'node:fs'
3
+ import { join, resolve as pathResolve } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { Router, MegaRouteError } from './router.js'
6
+
7
+ /**
8
+ * 앱의 routes/ 폴더 전체 스캔 → 각 파일을 Router 와 함께 실행.
9
+ *
10
+ * @param {Object} opts
11
+ * @param {import('fastify').FastifyInstance} opts.fastify
12
+ * @param {string} opts.appName
13
+ * @param {string} opts.routesDir - apps/<name>/routes 절대 경로
14
+ * @param {import('./mega-app.js').MegaApp} [opts.app] - 바인딩 앱 (HTTP ctx 의 db/cache/bus 접근자 출처, ADR-102)
15
+ * @returns {Promise<{ filesLoaded: number, routes: Array<{file: string}> }>}
16
+ */
17
+ export async function loadRoutes({ fastify, appName, routesDir, app }) {
18
+ const loaded = []
19
+ let routeFiles
20
+ try {
21
+ routeFiles = readdirSync(routesDir)
22
+ .filter((f) => f.endsWith('.js') && !f.endsWith('.test.js'))
23
+ .sort()
24
+ } catch (err) {
25
+ if (err.code === 'ENOENT') return { filesLoaded: 0, routes: [] }
26
+ throw err
27
+ }
28
+
29
+ for (const fileName of routeFiles) {
30
+ const absPath = pathResolve(join(routesDir, fileName))
31
+ const st = statSync(absPath)
32
+ if (!st.isFile()) continue
33
+
34
+ let mod
35
+ try {
36
+ mod = await import(pathToFileURL(absPath).href)
37
+ } catch (err) {
38
+ throw new MegaRouteError(
39
+ 'route.file_load_failed',
40
+ `Failed to load route file '${appName}/routes/${fileName}': ${err.message}`,
41
+ { cause: err },
42
+ )
43
+ }
44
+ if (typeof mod.default !== 'function') {
45
+ throw new MegaRouteError(
46
+ 'route.file_no_default',
47
+ `Route file '${appName}/routes/${fileName}' must export default a function (router) => { ... }.`,
48
+ )
49
+ }
50
+
51
+ const router = new Router({ fastify, appName, sourceFile: fileName, app })
52
+ await mod.default(router)
53
+ loaded.push({ file: fileName })
54
+ }
55
+
56
+ return { filesLoaded: loaded.length, routes: loaded }
57
+ }
@@ -0,0 +1,53 @@
1
+ // @ts-check
2
+ /**
3
+ * mega.config.js (Global) / apps/<name>/app.config.js (App + Shared-Reference) 의
4
+ * 키 분리 규약 (ADR-061). 알 수 없는 키 + 잘못된 스코프 → 부팅 throw.
5
+ *
6
+ * @module core/scope-registry
7
+ */
8
+
9
+ /** mega.config.js 최상위에만 허용되는 키 */
10
+ export const GLOBAL_ONLY_KEYS = Object.freeze([
11
+ 'services', // databases/caches/buses 그룹 정의
12
+ 'server', // port, cluster, sessionSecret
13
+ 'wsHub', // mega ws-hub 명령용 (ADR-068)
14
+ 'logger', // 전역 로거 sinks
15
+ 'apps', // 활성 앱 whitelist (ADR-066)
16
+ 'asp', // masterSecret 등 시크릿
17
+ 'health', // /health, /health/ready
18
+ 'tracing', // OpenTelemetry (ADR-077)
19
+ 'plugins', // 명시 등록 배열 (ADR-079)
20
+ 'jobs', // MegaJob 서브클래스 배열 — mega worker 가 소비 (ADR-123)
21
+ 'schedules', // MegaSchedule 서브클래스 배열 — mega scheduler 가 소비 (ADR-123)
22
+ 'workers', // MegaWorker 서브클래스 배열 — CPU 워커 풀, ctx.workers.<name> 배선 (ADR-124)
23
+ ])
24
+
25
+ /** apps/<name>/app.config.js 에만 허용되는 키 */
26
+ export const APP_ONLY_KEYS = Object.freeze([
27
+ 'name', // 폴더명 일치
28
+ 'hosts', // 도메인 매핑
29
+ 'cors', // 앱별 Fastify 인스턴스에만 적용
30
+ 'helmet',
31
+ 'rateLimit',
32
+ 'csrf',
33
+ 'session', // 앱별 세션 옵트인 (store/cookie/ttl/csrf 모드, ADR-129/046)
34
+ 'bodyLimits',
35
+ 'upload',
36
+ 'i18n', // 앱별 locale 선호
37
+ 'views', // 앱별 뷰 옵션
38
+ 'asp', // 앱별 ASP 옵트인 (Global asp.masterSecret 과 별개)
39
+ 'bridgeHub', // 어느 hub 에 연결 (ADR-065)
40
+ 'openapi', // App-only (ADR-070)
41
+ 'staticAssets', // App-only (ADR-071)
42
+ 'websocket', // 앱별 WS 옵션 (compression 등)
43
+ // Shared-Reference 키 (전역에 정의된 키 참조만 — ADR-061)
44
+ 'databases',
45
+ 'caches',
46
+ 'buses',
47
+ ])
48
+
49
+ /** Shared-Reference 키 — 전역 services 의 키만 참조 가능 */
50
+ export const SHARED_REFERENCE_KEYS = Object.freeze(['databases', 'caches', 'buses'])
51
+
52
+ export const ALL_GLOBAL_KEYS = Object.freeze(GLOBAL_ONLY_KEYS)
53
+ export const ALL_APP_KEYS = Object.freeze(APP_ONLY_KEYS)