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,253 @@
1
+ // @ts-check
2
+ /**
3
+ * ctx-builder — 요청 단위 컨텍스트(`ctx`)의 어댑터 lookup 배선 (ADR-102).
4
+ *
5
+ * canonical `ctx` 표면(docs/03 §581)의 데이터 어댑터 4종을 함수형으로 노출한다:
6
+ * - `ctx.db(alias)` → MegaDbAdapter (raw handle 은 `.native`)
7
+ * - `ctx.cache(alias)` → MegaCacheAdapter
8
+ * - `ctx.bus(alias)` → MegaBusAdapter
9
+ * - `ctx.lock(alias)` → MegaLockAdapter (분산 락, ADR-113)
10
+ *
11
+ * **별명 → globalKey → 공유 인스턴스** (글로벌 공유 모델, ADR-102):
12
+ * 앱은 `app.config.js` 에서 `databases: { alias: globalKey }` 로 별명을 선언하고, 코드에서는
13
+ * `ctx.db('alias')` 로 부른다. 빌더가 별명을 globalKey 로 바꿔 {@link module:adapters/adapter-manager}
14
+ * 의 공유 인스턴스를 돌려준다. **미선언 별명 / 미등록 키는 즉시 throw** (silent X — 오타가 런타임에
15
+ * 조용히 통과하지 않게, docs/04 L570 / ADR-007).
16
+ *
17
+ * @module core/ctx-builder
18
+ */
19
+ import { MegaConfigError } from '../errors/config-error.js'
20
+ import * as AdapterManager from '../adapters/adapter-manager.js'
21
+ import { contextProxy as workersContext } from './workers-manager.js'
22
+ import { tracer as megaTracer } from '../lib/mega-tracing.js'
23
+
24
+ /** ctx 도메인 함수 ↔ app.config 별명 맵 키. */
25
+ const DOMAIN_ALIAS_KEY = /** @type {const} */ ({ db: 'databases', cache: 'caches', bus: 'buses', lock: 'locks' })
26
+
27
+ /**
28
+ * i18n 미등록 앱의 `ctx.t` 폴백 — 번역 없이 defaultValue(있으면) 또는 key 를 그대로 돌려준다. i18n 옵트인
29
+ * 안 한 앱에서도 `ctx.t(code, message)` 호출이 깨지지 않게(에러 핸들러·코드 호환). i18next 시그니처와 동형:
30
+ * 2번째 인자가 문자열이면 defaultValue, 객체면 options(번역 없으니 무시하고 key 반환).
31
+ * @param {string} key @param {unknown} [defaultValue] @returns {string}
32
+ */
33
+ function passthroughT(key, defaultValue) {
34
+ return typeof defaultValue === 'string' ? defaultValue : key
35
+ }
36
+
37
+ /** 요청당 ctx 를 캐싱하는 비열거 키. 글로벌 미들웨어와 라우트 핸들러가 같은 ctx 를 공유하도록 한다. */
38
+ const HTTP_CTX_KEY = Symbol('mega.httpCtx')
39
+
40
+ /**
41
+ * `ctx.services.<name>` lazy DI proxy (ADR-148) — 요청별 서비스 인스턴스화·캐시.
42
+ *
43
+ * `app.serviceRegistry`(부팅 시 services-loader 가 만든 name→Class 맵)에서 클래스를 찾아 **요청 ctx 로**
44
+ * 인스턴스화하고, 같은 요청 안에서 한 번만 만들어 캐시한다(요청별 스코프 — 서비스가 ctx.log/db/session/
45
+ * user 에 바인딩되므로 싱글톤 불가). 서비스 간 합성(`this.services.<name>`)도 같은 proxy 를 거쳐 동일
46
+ * 인스턴스를 공유한다(mega-service.js `get services()` 가 `this.ctx.services` 위임).
47
+ *
48
+ * 미등록 이름 접근은 즉시 throw(`service.not_registered`, fail-fast — 오타가 조용히 undefined 로 통과하지
49
+ * 않게). 생성 중 재진입(A 생성자 → B → A)은 `service.circular_dependency` 로 차단한다(메서드 레벨 상호
50
+ * 호출은 정상 — 가드는 컨스트럭터 사이클만).
51
+ *
52
+ * @param {Record<string, any>} ctx - 요청 ctx(서비스 생성자에 전달).
53
+ * @param {import('./mega-app.js').MegaApp} app - serviceRegistry 보유 앱.
54
+ * @returns {Record<string, any>} 서비스 proxy.
55
+ */
56
+ function buildServicesProxy(ctx, app) {
57
+ const registry = /** @type {Map<string, Function>} */ (/** @type {any} */ (app).serviceRegistry)
58
+ /** @type {Map<string, any>} 요청 내 인스턴스 캐시. */
59
+ const instances = new Map()
60
+ /** @type {Set<string>} 생성 진행 중(순환 가드). */
61
+ const constructing = new Set()
62
+
63
+ /** @param {string} name @returns {any} */
64
+ function resolve(name) {
65
+ if (instances.has(name)) return instances.get(name)
66
+ const Cls = registry.get(name)
67
+ if (!Cls) {
68
+ throw new MegaConfigError(
69
+ 'service.not_registered',
70
+ `ctx.services.${name} — app '${app.name}' 에 '${name}' 서비스가 등록돼 있지 않습니다. 등록됨: [${[...registry.keys()].join(', ') || '(none)'}].`,
71
+ { details: { app: app.name, name } },
72
+ )
73
+ }
74
+ if (constructing.has(name)) {
75
+ throw new MegaConfigError(
76
+ 'service.circular_dependency',
77
+ `ctx.services.${name} — 생성 중 순환 의존: [${[...constructing, name].join(' → ')}]. 다른 서비스는 컨스트럭터가 아닌 메서드에서 호출하세요.`,
78
+ { details: { app: app.name, cycle: [...constructing, name] } },
79
+ )
80
+ }
81
+ constructing.add(name)
82
+ try {
83
+ const instance = new (/** @type {any} */ (Cls))(ctx, { app })
84
+ instances.set(name, instance)
85
+ return instance
86
+ } finally {
87
+ constructing.delete(name)
88
+ }
89
+ }
90
+
91
+ // 빈 타깃 proxy — get(인스턴스 해석) + has(`'name' in ctx.services`)만 구현. ownKeys/enumerate 는
92
+ // 의도(접근 시점에만 인스턴스화) 보존을 위해 노출하지 않는다(열거가 전체 서비스를 생성하지 않게).
93
+ return new Proxy(
94
+ {},
95
+ {
96
+ get(_t, prop) {
97
+ if (typeof prop !== 'string') return undefined // Symbol(then 등) — non-thenable 유지
98
+ return resolve(prop)
99
+ },
100
+ has(_t, prop) {
101
+ return typeof prop === 'string' && registry.has(prop)
102
+ },
103
+ },
104
+ )
105
+ }
106
+
107
+ /**
108
+ * @typedef {object} AdapterAliasMaps - 앱의 `app.config.js` 별명 선언 (alias → globalKey).
109
+ * @property {Record<string, string>} [databases]
110
+ * @property {Record<string, string>} [caches]
111
+ * @property {Record<string, string>} [buses]
112
+ * @property {Record<string, string>} [locks]
113
+ */
114
+
115
+ /**
116
+ * @typedef {object} AdapterAccessors
117
+ * @property {(alias: string) => import('../adapters/mega-adapter.js').MegaAdapter} db
118
+ * @property {(alias: string) => import('../adapters/mega-adapter.js').MegaAdapter} cache
119
+ * @property {(alias: string) => import('../adapters/mega-adapter.js').MegaAdapter} bus
120
+ * @property {(alias: string) => import('../adapters/mega-adapter.js').MegaAdapter} lock
121
+ */
122
+
123
+ /**
124
+ * 앱의 별명 맵으로 `db/cache/bus/lock` 접근자 4종을 만든다. 접근자는 요청과 무관하게 안정적이므로
125
+ * **앱당 한 번** 만들어 재사용한다(MegaApp 생성자). 각 접근자는 호출 시 별명→globalKey→공유
126
+ * 인스턴스로 해석하고, 미선언 별명이면 throw.
127
+ *
128
+ * @param {AdapterAliasMaps} [aliasMaps] - 앱 별명 선언. 없으면 모든 접근자가 미선언 throw.
129
+ * @param {string} [appName] - 에러 메시지용 앱 이름.
130
+ * @returns {AdapterAccessors}
131
+ */
132
+ export function buildAdapterAccessors(aliasMaps = {}, appName = '(unknown)') {
133
+ /**
134
+ * @param {'db'|'cache'|'bus'|'lock'} domain
135
+ * @returns {(alias: string) => import('../adapters/mega-adapter.js').MegaAdapter}
136
+ */
137
+ const make = (domain) => {
138
+ const aliasMap = aliasMaps[DOMAIN_ALIAS_KEY[domain]] ?? {}
139
+ return (alias) => {
140
+ if (typeof alias !== 'string' || alias.length === 0) {
141
+ throw new MegaConfigError('adapter.invalid_key', `ctx.${domain}(key) — key must be a non-empty string.`, {
142
+ details: { domain, alias },
143
+ })
144
+ }
145
+ const globalKey = aliasMap[alias]
146
+ if (globalKey === undefined) {
147
+ throw new MegaConfigError(
148
+ 'adapter.not_registered',
149
+ `ctx.${domain}('${alias}') — '${alias}' is not declared in app '${appName}' config.${DOMAIN_ALIAS_KEY[domain]}. Declared: [${Object.keys(aliasMap).join(', ') || '(none)'}].`,
150
+ { details: { domain, alias, app: appName, declared: Object.keys(aliasMap) } },
151
+ )
152
+ }
153
+ return AdapterManager.get(domain, globalKey) // 별명은 있으나 인스턴스 미등록이면 여기서 throw
154
+ }
155
+ }
156
+ return { db: make('db'), cache: make('cache'), bus: make('bus'), lock: make('lock') }
157
+ }
158
+
159
+ /**
160
+ * HTTP 요청 단위 ctx 를 만든다 (canonical handler 시그니처 `(req, res, ctx)`, ADR-074/docs/03).
161
+ *
162
+ * canonical ctx 표면: `app / log / requestId / req / reply` + `db/cache/bus` 접근자 +
163
+ * `workers`(CPU 워커 풀, ADR-124) + `tracer`(사용자 직접 span — `ctx.tracer.span`,
164
+ * ADR-126). (i18n `t`, `session` 객체, `services` 자동 DI 등 나머지 canonical 필드도 같은 방식으로 추가된다.)
165
+ *
166
+ * `app` 이 null(standalone Router — 앱에 바인딩 안 된 테스트 경로)이면 `db/cache/bus` 접근자는
167
+ * 생략된다(해당 ctx 로는 어댑터 lookup 불가). `workers` 는 글로벌 자원이라 app 무관하게 항상 노출한다
168
+ * (미등록 이름 접근 시 worker.not_registered fail-fast — 워커 0개면 접근 자체가 없어 무해).
169
+ *
170
+ * @param {object} args
171
+ * @param {import('./mega-app.js').MegaApp | null} args.app
172
+ * @param {import('fastify').FastifyRequest} args.req
173
+ * @param {import('fastify').FastifyReply} args.reply
174
+ * @returns {Record<string, any>}
175
+ */
176
+ export function buildHttpCtx({ app, req, reply }) {
177
+ /** @type {Record<string, any>} */
178
+ const ctx = {
179
+ app: app ?? null,
180
+ log: req.log,
181
+ requestId: req.id,
182
+ req,
183
+ reply,
184
+ workers: workersContext(), // ctx.workers.<name>.run(task) — 글로벌 CPU 워커 풀(ADR-124).
185
+ tracer: megaTracer, // ctx.tracer.span(name, fn) — 사용자 직접 span(ADR-126). 옵트인 OFF 면 0 비용 no-op.
186
+ session: /** @type {any} */ (req).session ?? null, // ctx.session — 세션 미들웨어가 채운 요청별 세션 객체(ADR-129). 미활성/미로드 시 null.
187
+ lang: /** @type {any} */ (req).lang ?? null, // ctx.lang — i18n 미들웨어가 쿠키로 결정한 현재 언어(ADR-038). 미활성 시 null.
188
+ // ctx.t(key, defaultValue?, params?) — server scope 번역(docs/03 §632, ADR-135). i18n 미등록이면 passthrough.
189
+ t: /** @type {any} */ (req).t ?? passthroughT,
190
+ // ctx.render(view, data?, opts?) — reply.render(정본 res.render, ADR-011/136)로 위임. 템플릿 옵트인 안 한
191
+ // 앱은 reply.render 미정의 → 명확한 fail-fast(template.not_configured). reply.render 가 i18n t/lang 자동 병합.
192
+ render: (/** @type {string} */ view, /** @type {object} */ data, /** @type {any} */ opts) => {
193
+ if (typeof (/** @type {any} */ (reply).render) !== 'function') {
194
+ throw new MegaConfigError('template.not_configured', `ctx.render('${view}') — app '${app?.name ?? '(unknown)'}' has no views config (opt-in template; set app.config views.dir).`, {
195
+ details: { view, app: app?.name ?? null },
196
+ })
197
+ }
198
+ return /** @type {any} */ (reply).render(view, data, opts)
199
+ },
200
+ ...(app?.adapterAccessors ?? {}),
201
+ }
202
+ // ctx.bruteForce — MegaBruteForce 단축(ADR-049/130). **lazy getter** 라 접근할 때만 app.bruteForce 가
203
+ // caches.rate 를 해석한다(브루트포스 미사용 앱은 rate 캐시 없어도 무해). app 미바인딩(standalone)이면 생략.
204
+ // enumerable:false (L-2) — ctx spread/열거({...ctx}, Object.keys) 시 getter 가 실행돼 의도치 않게
205
+ // app.bruteForce 해석(미설정 시 throw)되는 걸 막는다. 명시 접근(ctx.bruteForce)으로만 평가.
206
+ if (app) {
207
+ Object.defineProperty(ctx, 'bruteForce', { get: () => app.bruteForce, enumerable: false, configurable: true })
208
+ }
209
+ // ctx.user — 인증 사용자. getter 는 req.user 를 그대로 반영(인증 가드 mega-framework/auth(ADR-143)는
210
+ // 라우트 before 경로에서 req.user 에 심는다), setter 는 req.user 에 위임(글로벌 미들웨어가 ctx.user 에
211
+ // 직접 심는 ADR-134 패턴 호환). 두 경로가 같은 저장소(req.user)를 공유 → 미들웨어→핸들러 일관(미인증 null).
212
+ Object.defineProperty(ctx, 'user', {
213
+ get: () => /** @type {any} */ (req).user ?? null,
214
+ set: (v) => {
215
+ ;/** @type {any} */ (req).user = v
216
+ },
217
+ enumerable: true,
218
+ configurable: true,
219
+ })
220
+ // ctx.services.<name> — 요청별 lazy 서비스 DI(ADR-148). app 미바인딩(standalone)이거나 등록 서비스가
221
+ // 없으면 생략한다(테스트는 MegaTest 가 services mock 을 ctx 에 직접 주입). enumerable:false — ctx 열거
222
+ // ({...ctx}/Object.keys)에 노출하지 않아 기존 ctx 표면 스냅샷·spread 동작을 바꾸지 않는다(명시 접근만).
223
+ if (app && /** @type {any} */ (app).serviceRegistry?.size > 0) {
224
+ Object.defineProperty(ctx, 'services', {
225
+ value: buildServicesProxy(ctx, app),
226
+ enumerable: false,
227
+ configurable: true,
228
+ })
229
+ }
230
+ return ctx
231
+ }
232
+
233
+ /**
234
+ * 요청당 ctx 를 **한 번만** 만들고 `req` 에 캐싱해 재사용한다(ADR-134). 글로벌 미들웨어(preHandler)와
235
+ * 라우트 핸들러가 같은 요청에서 호출하면 **동일 ctx 객체**를 받으므로, 미들웨어가 ctx 에 심은 값
236
+ * (예: `ctx.user`)을 핸들러가 그대로 본다. 첫 호출이 build, 이후 호출은 캐시 반환.
237
+ *
238
+ * 캐시 키는 비열거 Symbol 이라 `{...ctx}`/`Object.keys(req)` 에 노출되지 않는다. `req` 는 Fastify 가
239
+ * 요청마다 새로 만드는 객체라 요청 스코프로 자연히 격리된다(요청 종료 시 GC).
240
+ *
241
+ * @param {object} args
242
+ * @param {import('./mega-app.js').MegaApp | null} args.app
243
+ * @param {import('fastify').FastifyRequest} args.req
244
+ * @param {import('fastify').FastifyReply} args.reply
245
+ * @returns {Record<string, any>}
246
+ */
247
+ export function getHttpCtx({ app, req, reply }) {
248
+ const cached = /** @type {any} */ (req)[HTTP_CTX_KEY]
249
+ if (cached) return cached
250
+ const ctx = buildHttpCtx({ app, req, reply })
251
+ Object.defineProperty(req, HTTP_CTX_KEY, { value: ctx, enumerable: false, configurable: true })
252
+ return ctx
253
+ }
@@ -0,0 +1,88 @@
1
+ // @ts-check
2
+ /**
3
+ * 응답 envelope 헬퍼 (ADR-014, ADR-018).
4
+ *
5
+ * 모든 HTTP 응답은 `{ ok, data|error, meta }` 형태로 통일된다.
6
+ * wrapEnvelope 는 preSerialization 의 마지막 단계에서 raw data 를 감싸고,
7
+ * errorEnvelope 는 글로벌 에러 핸들러에서 에러를 감싼다.
8
+ *
9
+ * @module core/envelope
10
+ */
11
+
12
+ /** request 시작 시간 기록용 symbol (meta.took_ms 계산). reply 객체에 부착. */
13
+ export const REPLY_START_SYMBOL = Symbol('mega.reply.start')
14
+
15
+ /**
16
+ * 자동 envelope (ADR-018). preSerialization 훅의 마지막 단계에서 호출되어
17
+ * raw data → `{ ok:true, data, meta }`.
18
+ *
19
+ * transform 미들웨어(ADR-091)가 먼저 raw data 를 변환했으면, 그 변환 결과 위에 envelope 만 감쌈
20
+ * (실행 순서: transform → wrap, ADR-076 / ADR-091).
21
+ *
22
+ * 이미 envelope 형태로 보낸 경우(`{ ok:false, error: ... }` 등 명시) 는 그대로 통과 —
23
+ * 글로벌 에러 핸들러가 만든 error envelope 의 이중 wrap 을 방지.
24
+ *
25
+ * @param {Record<string, any>} req - Fastify request
26
+ * @param {Record<string | symbol, any>} reply - Fastify reply
27
+ * @param {any} payload - 핸들러/transform 이 만든 raw data 또는 이미 만들어진 envelope
28
+ * @returns {Object | any} envelope 또는 (이미 envelope 면) 원본 payload
29
+ */
30
+ export function wrapEnvelope(req, reply, payload) {
31
+ if (isEnvelope(payload)) return payload
32
+ return {
33
+ ok: true,
34
+ data: payload,
35
+ meta: buildMeta(req, reply),
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 에러 envelope (ADR-014). 글로벌 에러 핸들러가 사용.
41
+ * @param {Record<string, any>} req
42
+ * @param {Record<string | symbol, any>} reply
43
+ * @param {{ code: string, message?: string, details?: any }} error
44
+ * @returns {{ ok: false, error: { code: string, message: string, details?: any }, meta: Object }}
45
+ */
46
+ export function errorEnvelope(req, reply, error) {
47
+ return {
48
+ ok: false,
49
+ error: {
50
+ code: error.code,
51
+ message: error.message ?? '',
52
+ ...(error.details !== undefined ? { details: error.details } : {}),
53
+ },
54
+ meta: buildMeta(req, reply),
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 이미 envelope 모양인지 판별 — `ok: boolean` + (`data` | `error`) 키 보유.
60
+ * @param {any} payload
61
+ * @returns {boolean}
62
+ */
63
+ function isEnvelope(payload) {
64
+ return (
65
+ payload != null &&
66
+ typeof payload === 'object' &&
67
+ !Array.isArray(payload) &&
68
+ typeof payload.ok === 'boolean' &&
69
+ ('data' in payload || 'error' in payload)
70
+ )
71
+ }
72
+
73
+ /**
74
+ * meta 빌드 — request_id (있으면) + took_ms (onRequest 에서 시작 시각 기록됐으면).
75
+ * @param {Record<string, any>} req
76
+ * @param {Record<string | symbol, any>} reply
77
+ * @returns {{ request_id?: string, took_ms?: number }}
78
+ */
79
+ function buildMeta(req, reply) {
80
+ /** @type {{ request_id?: string, took_ms?: number }} */
81
+ const meta = {}
82
+ if (req?.id) meta.request_id = String(req.id)
83
+ const startedAt = reply?.[REPLY_START_SYMBOL]
84
+ if (typeof startedAt === 'number') {
85
+ meta.took_ms = Date.now() - startedAt
86
+ }
87
+ return meta
88
+ }
@@ -0,0 +1,116 @@
1
+ // @ts-check
2
+ /**
3
+ * 글로벌 에러 핸들러 (ADR-025, ADR-090). Fastify `setErrorHandler` 에 등록.
4
+ *
5
+ * 던져진 모든 에러를 envelope error (ADR-014) 로 통일 변환한다.
6
+ *
7
+ * @module core/error-mapper
8
+ */
9
+ import { MegaHttpError, MegaInternalError } from '../errors/http-errors.js'
10
+ import { MegaError } from '../errors/mega-error.js'
11
+ import { ajvErrorToValidationError } from './ajv-mapper.js'
12
+ import { errorEnvelope } from './envelope.js'
13
+
14
+ /**
15
+ * Fastify setErrorHandler 에 등록할 핸들러 빌더.
16
+ *
17
+ * 우선순위:
18
+ * 1) AJV validation 에러 → MegaValidationError 변환 (ADR-090)
19
+ * 2) MegaHttpError → 그대로 envelope (status/code/message/details 보존)
20
+ * 3) MegaError → 500 + code/message 보존 (도메인 에러지만 HTTP status 없음)
21
+ * 4) 그 외 일반 Error → MegaInternalError (스택·메시지는 로그만, envelope 엔 마스킹)
22
+ *
23
+ * @param {{ exposeInternalDetails?: boolean }} [opts]
24
+ * - exposeInternalDetails: true 면 일반 Error 의 message 를 envelope 에 노출.
25
+ * 기본 false (보안 — 내부 구현 누출 방지).
26
+ * @returns {(error: Error, req: any, reply: any) => any}
27
+ */
28
+ export function buildErrorHandler({ exposeInternalDetails = false } = {}) {
29
+ return function errorHandler(error, req, reply) {
30
+ // 에러 캡처 시점 debug 로그 (시크릿 제외, 핵심 필드만).
31
+ const log = req?.log
32
+ log?.debug?.(
33
+ { err: error, route: req?.url, method: req?.method },
34
+ 'error-handler enter',
35
+ )
36
+
37
+ // 1) AJV 변환 시도 → 2~4) MegaHttpError 정규화
38
+ const fromAjv = ajvErrorToValidationError(error)
39
+ const e = fromAjv ?? toMegaHttpError(error, exposeInternalDetails)
40
+
41
+ reply.code(e.status)
42
+ return errorEnvelope(req, reply, {
43
+ code: e.code,
44
+ message: translateMessage(req, e),
45
+ details: e.details,
46
+ })
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 에러 메시지를 i18n 으로 번역한다(ADR-016/135 — 에러 코드 ↔ server scope locale 키 1:1 매핑).
52
+ *
53
+ * i18n 옵트인 앱은 요청에 `req.t`(server scope 번역기)가 붙어 있다. 에러 `code`(예: `user.not_found`)를
54
+ * locale 키로 lookup 하되, 키가 없으면 원본 `message` 를 그대로 쓴다(`defaultValue`). `details` 를
55
+ * 보간 params 로 넘겨 `{{id}}` 류 치환을 지원한다. i18n 미등록(req.t 없음)이거나 i18n 이전 단계 에러면
56
+ * 원본 message 반환(하위 호환). dev 면 누락 키가 saveMissing 으로 자동 수집된다.
57
+ *
58
+ * @param {any} req - Fastify 요청(i18n 등록 시 `req.t` 보유).
59
+ * @param {{ code: string, message: string, details?: any }} e - 정규화된 에러.
60
+ * @returns {string} 번역된(또는 원본) 메시지.
61
+ */
62
+ function translateMessage(req, e) {
63
+ const t = req?.t
64
+ if (typeof t !== 'function') return e.message
65
+ // i18next: t(key, defaultValue, params) — 키 있으면 번역, 없으면 defaultValue(=원본 message).
66
+ const params = e.details && typeof e.details === 'object' ? e.details : {}
67
+ return t(e.code, e.message, params)
68
+ }
69
+
70
+ /**
71
+ * 임의의 에러를 MegaHttpError 로 정규화.
72
+ * @param {any} error
73
+ * @param {boolean} exposeInternalDetails
74
+ * @returns {MegaHttpError}
75
+ */
76
+ function toMegaHttpError(error, exposeInternalDetails) {
77
+ if (error instanceof MegaHttpError) return error
78
+ if (error instanceof MegaError) {
79
+ // 도메인 에러지만 HTTP status 없음 → 500 (code/message/details 보존)
80
+ return new MegaInternalError(error.code, error.message, {
81
+ details: error.details,
82
+ cause: error.cause ?? error,
83
+ })
84
+ }
85
+ // Fastify/플러그인 네이티브 4xx 에러(FST_CSRF_*, 413 body-too-large, 415 unsupported-media 등)는
86
+ // 클라이언트 잘못이라 status·메시지를 보존한다(ADR-127). 보존 안 하면 모든 4xx 가 500 으로 뭉개져
87
+ // 디버깅·클라 분기가 깨진다. 5xx/status 없는 일반 Error 만 아래 generic 500 으로 떨어진다.
88
+ const sc = Number(error?.statusCode)
89
+ if (Number.isInteger(sc) && sc >= 400 && sc < 500) {
90
+ return new MegaHttpError(sc, fastifyErrorCode(error?.code), error?.message || 'Request error', {})
91
+ }
92
+ // 일반 Error — 내부 상세 노출 금지가 디폴트 (보안)
93
+ return new MegaInternalError(
94
+ 'server.internal',
95
+ exposeInternalDetails ? error?.message || 'Internal server error' : 'Internal server error',
96
+ { cause: error },
97
+ )
98
+ }
99
+
100
+ /**
101
+ * Fastify/플러그인 네이티브 에러 코드(`FST_CSRF_INVALID_TOKEN` 등)를 envelope `domain.error` 코드로
102
+ * 매핑한다(ADR-016). CSRF 는 명시 매핑, 그 외 알려지지 않은 `FST_*` 는 `request.rejected` 로 통일한다.
103
+ *
104
+ * @param {unknown} code - error.code(있으면).
105
+ * @returns {string} envelope 코드.
106
+ */
107
+ function fastifyErrorCode(code) {
108
+ switch (code) {
109
+ case 'FST_CSRF_INVALID_TOKEN':
110
+ return 'csrf.invalid_token'
111
+ case 'FST_CSRF_MISSING_SECRET':
112
+ return 'csrf.missing_secret'
113
+ default:
114
+ return 'request.rejected'
115
+ }
116
+ }
@@ -0,0 +1,69 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaApp HTML 폼 바디 파싱 자동 등록 — `@fastify/formbody` 통합.
4
+ *
5
+ * # 무엇인가 (중학생용 설명)
6
+ * 웹 페이지의 `<form>` 을 제출하면 브라우저는 입력값을 `application/x-www-form-urlencoded`
7
+ * (예: `name=Ada&email=ada%40x.com`) 형식으로 보낸다. Fastify 는 기본으로 JSON·text 만 파싱하므로
8
+ * 이 형식은 그냥 두면 `req.body` 가 비어 있다. 본 모듈은 검증된 공식 플러그인 `@fastify/formbody` 를
9
+ * 등록해 urlencoded 바디를 객체로 파싱해 `req.body` 에 채운다(서버사이드 폼 처리 = MPA).
10
+ *
11
+ * # 옵트인 조건 (views 연동)
12
+ * 서버사이드 뷰(`views.dir`)를 켠 앱만 등록한다. 서버 렌더 뷰가 있다는 건 HTML 폼 제출을 받을
13
+ * 가능성이 높다는 뜻이라, 폼 파싱을 뷰 옵트인에 묶는다(별도 config 키 없이). JSON 전용 API 앱은
14
+ * 기존처럼 JSON·text 만 파싱(동작 변화 없음). urlencoded 가 필요한 API 앱은 직접
15
+ * `fastify.addContentTypeParser(...)` 로 등록할 수 있다(formbody 미등록이라 충돌 없음).
16
+ *
17
+ * # CSRF 통합
18
+ * `security.js` 가 `application/x-www-form-urlencoded` 를 **폼**으로 분류해 CSRF 토큰을 검증한다(ADR-051).
19
+ * 폼 파싱과 무관하게 적용되므로, 폼은 `_csrf` 토큰을 함께 보내야 한다(csrf 활성 시).
20
+ *
21
+ * @module core/formbody
22
+ * @see ADR-151
23
+ * @see https://github.com/fastify/fastify-formbody (@fastify/formbody v8)
24
+ */
25
+ import fastifyFormbody from '@fastify/formbody'
26
+
27
+ /**
28
+ * 폼 바디 파서 자동 등록 결과 요약(디버그·테스트용).
29
+ * @typedef {Object} FormbodySummary
30
+ * @property {boolean} enabled - 등록 여부.
31
+ */
32
+
33
+ /**
34
+ * views 설정이 서버사이드 렌더를 켰는지 (Boolean — `has*`). `views.dir` 가 비지 않은 문자열일 때만 true.
35
+ * registerTemplate 의 옵트인 판정과 같은 기준이라 폼 파싱이 뷰 등록과 정확히 짝을 이룬다.
36
+ * @param {unknown} views
37
+ * @returns {boolean}
38
+ */
39
+ export function hasServerViews(views) {
40
+ return (
41
+ views != null &&
42
+ typeof views === 'object' &&
43
+ typeof /** @type {Record<string, any>} */ (views).dir === 'string' &&
44
+ /** @type {Record<string, any>} */ (views).dir.length > 0
45
+ )
46
+ }
47
+
48
+ /**
49
+ * Fastify 인스턴스에 `@fastify/formbody` 를 등록한다 — 서버사이드 뷰를 켠 앱에 한해 옵트인.
50
+ *
51
+ * `views` 가 없으면 **미등록**(JSON·text 만 파싱, 기존 동작 유지). 호출 시점은 라우트 등록 이전이어야
52
+ * 한다(content-type 파서는 글로벌이라 등록 후 도착하는 모든 요청에 적용) — MegaApp 생성자가 그 지점이다.
53
+ *
54
+ * @param {import('fastify').FastifyInstance} fastify - 대상 앱 Fastify 인스턴스.
55
+ * @param {Object} opts
56
+ * @param {unknown} opts.views - `MegaViewsConfig`(dir/layoutDir/...). 서버 뷰 옵트인 판정에 쓴다.
57
+ * @param {string} [opts.appName] - 앱 이름(로그용).
58
+ * @param {{ debug?: Function }} [opts.logger] - 흐름 길목 debug 로그(선택).
59
+ * @returns {FormbodySummary}
60
+ */
61
+ export function registerFormbody(fastify, { views, appName = '(unknown)', logger } = /** @type {any} */ ({})) {
62
+ if (!hasServerViews(views)) {
63
+ return { enabled: false }
64
+ }
65
+ // 기본 옵션 — urlencoded 를 Node querystring 으로 파싱해 req.body 객체로. bodyLimit 등은 Fastify 전역 기본 적용.
66
+ fastify.register(fastifyFormbody)
67
+ logger?.debug?.({ app: appName }, 'formbody.registered')
68
+ return { enabled: true }
69
+ }