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,157 @@
1
+ // @ts-check
2
+ /**
3
+ * 어댑터 드라이버 레지스트리 (ADR-044, ADR-079).
4
+ *
5
+ * driver 이름 → 어댑터 클래스 매핑을 관리한다.
6
+ * - **빌트인 8종** (`postgres / mongodb / mariadb / sqlite / redis / file / nats / redlock`) 은
7
+ * 각 구체 어댑터 모듈이 import 시 `register()` 로 자기 자신을 등록한다.
8
+ * → 본 배럴은 매핑 *메커니즘* 만 제공하고, 구체 클래스는 위 어댑터 모듈들이 등록한다.
9
+ * - **3rd party** 는 `config.adapters.register: { 'cassandra': MegaCassandraAdapter }` 를
10
+ * {@link registerFromConfig} 로 주입 (ADR-044).
11
+ *
12
+ * 부팅 시 driver 매핑으로 `new AdapterClass(config)` 한다 (08-class-specs §3.2 lifecycle 1단계).
13
+ *
14
+ * 사용:
15
+ * register('sqlite', MegaSqliteAdapter) // 빌트인 자기 등록
16
+ * registerFromConfig(config.adapters?.register) // 3rd party 주입
17
+ * const AdapterClass = resolve('sqlite') // 부팅 시 클래스 lookup
18
+ *
19
+ * @module adapters/registry
20
+ */
21
+ import { MegaInternalError, MegaValidationError } from '../errors/http-errors.js'
22
+ import { MegaAdapter } from './mega-adapter.js'
23
+
24
+ /**
25
+ * 빌트인 driver 예약어 (ADR-044, ADR-082 file 포함, ADR-113 redlock 포함). 구체 클래스는 각 Step 에서 등록.
26
+ * 3rd party 가 이 이름을 점유하지 못하도록 예약 — `registerFromConfig` 가 거부.
27
+ * @type {ReadonlySet<string>}
28
+ */
29
+ export const BUILTIN_DRIVERS = new Set([
30
+ 'postgres',
31
+ 'mongodb',
32
+ 'mariadb',
33
+ 'sqlite',
34
+ 'redis',
35
+ 'file',
36
+ 'nats',
37
+ 'redlock',
38
+ ])
39
+
40
+ /** @type {Map<string, typeof MegaAdapter>} */
41
+ const registry = new Map()
42
+
43
+ /**
44
+ * driver 이름이 MegaAdapter 서브클래스 클래스인지 검증.
45
+ * @param {string} driver
46
+ * @param {unknown} AdapterClass
47
+ * @returns {void}
48
+ */
49
+ function assertValid(driver, AdapterClass) {
50
+ if (typeof driver !== 'string' || driver.length === 0) {
51
+ throw new MegaValidationError('adapter.invalid_driver', 'driver name must be a non-empty string', {
52
+ details: { driver },
53
+ })
54
+ }
55
+ // MegaAdapter 자신(추상) 또는 그 서브클래스 생성자만 허용.
56
+ const isAdapterClass =
57
+ typeof AdapterClass === 'function' &&
58
+ (AdapterClass === MegaAdapter || AdapterClass.prototype instanceof MegaAdapter)
59
+ if (!isAdapterClass) {
60
+ throw new MegaValidationError(
61
+ 'adapter.invalid_class',
62
+ `Adapter for driver "${driver}" must be a class extending MegaAdapter.`,
63
+ { details: { driver } },
64
+ )
65
+ }
66
+ }
67
+
68
+ /**
69
+ * driver → 어댑터 클래스 등록. 이미 같은 driver 가 **다른 클래스**로 등록돼 있으면 throw
70
+ * (silent override 방지). 같은 클래스 재등록은 idempotent no-op.
71
+ *
72
+ * @param {string} driver - driver 이름 (예: 'postgres').
73
+ * @param {typeof MegaAdapter} AdapterClass - MegaAdapter 를 상속한 어댑터 클래스.
74
+ * @returns {void}
75
+ */
76
+ export function register(driver, AdapterClass) {
77
+ assertValid(driver, AdapterClass)
78
+ const existing = registry.get(driver)
79
+ if (existing !== undefined && existing !== AdapterClass) {
80
+ throw new MegaInternalError(
81
+ 'adapter.driver_conflict',
82
+ `driver "${driver}" is already registered to ${existing.name}; cannot re-register to ${AdapterClass.name}.`,
83
+ { details: { driver, existing: existing.name, incoming: AdapterClass.name } },
84
+ )
85
+ }
86
+ registry.set(driver, AdapterClass)
87
+ }
88
+
89
+ /**
90
+ * `config.adapters.register` 맵을 일괄 등록 (ADR-044 3rd party). 빌트인 예약어 점유 시 throw.
91
+ *
92
+ * @param {Record<string, typeof MegaAdapter>} [registerMap]
93
+ * @returns {void}
94
+ */
95
+ export function registerFromConfig(registerMap) {
96
+ if (registerMap === undefined || registerMap === null) return
97
+ // L5: 배열도 typeof 'object' 라 그냥 두면 Object.entries 가 인덱스를 driver 이름으로 등록한다.
98
+ // adapters.register 는 `{ driver: Class }` 평탄 객체만 허용 — 배열은 명시 거부.
99
+ if (typeof registerMap !== 'object' || Array.isArray(registerMap)) {
100
+ throw new MegaValidationError('adapter.invalid_register_map', 'adapters.register must be a plain object', {
101
+ details: { type: Array.isArray(registerMap) ? 'array' : typeof registerMap },
102
+ })
103
+ }
104
+ for (const [driver, AdapterClass] of Object.entries(registerMap)) {
105
+ if (BUILTIN_DRIVERS.has(driver)) {
106
+ throw new MegaInternalError(
107
+ 'adapter.driver_conflict',
108
+ `driver "${driver}" is a builtin and cannot be overridden via adapters.register.`,
109
+ { details: { driver } },
110
+ )
111
+ }
112
+ register(driver, AdapterClass)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * driver 이름으로 등록된 어댑터 클래스 조회. 없으면 `adapter.unknown_driver` throw.
118
+ *
119
+ * @param {string} driver
120
+ * @returns {typeof MegaAdapter}
121
+ */
122
+ export function resolve(driver) {
123
+ const AdapterClass = registry.get(driver)
124
+ if (AdapterClass === undefined) {
125
+ throw new MegaValidationError(
126
+ 'adapter.unknown_driver',
127
+ `Unknown adapter driver "${driver}". Registered: [${list().join(', ') || '(none)'}].`,
128
+ { details: { driver, registered: list() } },
129
+ )
130
+ }
131
+ return AdapterClass
132
+ }
133
+
134
+ /**
135
+ * driver 가 등록돼 있는지 (Boolean — `has*`, ADR-036).
136
+ * @param {string} driver
137
+ * @returns {boolean}
138
+ */
139
+ export function has(driver) {
140
+ return registry.has(driver)
141
+ }
142
+
143
+ /**
144
+ * 등록된 driver 이름 목록.
145
+ * @returns {string[]}
146
+ */
147
+ export function list() {
148
+ return [...registry.keys()]
149
+ }
150
+
151
+ /**
152
+ * 테스트용 reset — 등록 전부 비움.
153
+ * @returns {void}
154
+ */
155
+ export function _reset() {
156
+ registry.clear()
157
+ }
@@ -0,0 +1,309 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaSqliteAdapter — SQLite 어댑터 (`better-sqlite3` 래퍼, ADR-105).
4
+ *
5
+ * **첫 실 어댑터**. docker 없이 in-memory(`:memory:`) / file-based DB 로
6
+ * `MegaDbAdapter` 베이스 + `MegaModel` 정합을 끝단 검증하는 기준점이다.
7
+ *
8
+ * # 표준 표면 (MegaDbAdapter 상속)
9
+ * - `_connect()` — `new Database(filename, opts)` (driver 는 connect 시점 lazy import).
10
+ * - `_disconnect()`— `db.close()`.
11
+ * - `_native()` — `better-sqlite3` Database 인스턴스(raw handle, ADR-009). `MegaModel.db` 가 노출.
12
+ * - `healthCheck()`— `SELECT 1` 실행으로 실제 응답성 확인 (베이스 디폴트=상태만 → override).
13
+ * - `getStats()` — 베이스 stats + sqlite 특화(filename / inMemory / readonly / journalMode).
14
+ * - `withTransaction(fn)` — 명시적 트랜잭션 경계 (ADR-010). 아래 "트랜잭션 패턴" 참조.
15
+ *
16
+ * # 트랜잭션 패턴 — manual `BEGIN/COMMIT/ROLLBACK` (중요)
17
+ * `better-sqlite3` 의 `db.transaction(fn)` 은 **동기 콜백 전용**이다 — 공식 문서가
18
+ * "async 함수는 첫 `await` 에서 반환되므로 실제 비동기 코드 실행 전에 트랜잭션이 이미
19
+ * commit 된다"며 async 콜백을 명시적으로 금지한다. 그러나 `MegaDbAdapter.withTransaction(fn)`
20
+ * 계약의 `fn` 은 `async` 일 수 있다(`Promise<T>` 반환). 두 모델을 정합시키기 위해
21
+ * `db.transaction()` 대신 **수동 `db.exec('BEGIN' | 'COMMIT' | 'ROLLBACK')`** 로 경계를 긋는다.
22
+ * 성공 시 COMMIT, `fn` throw 시 ROLLBACK 후 원본 에러 re-throw (ADR-010).
23
+ *
24
+ * # nested / 동시 트랜잭션 정책 — 재진입 거부 (ADR-105)
25
+ * SQLite 는 본 어댑터당 **단일 연결**(`better-sqlite3` 는 connection-per-Database)이다. 이미
26
+ * 열린 트랜잭션 위에서 또 `BEGIN` 을 걸면 SQLite 가 거부한다. 게다가 `withTransaction(fn)` 은
27
+ * async 라 `fn` 의 `await` 동안 다른 `withTransaction` 호출이 끼어들 수 있다(인터리빙). 두 경우
28
+ * 모두 `db.inTransaction`(better-sqlite3 공식 프로퍼티)으로 감지해 즉시
29
+ * `adapter.nested_transaction_unsupported` 를 throw 한다 — postgres 의 SAVEPOINT 기반 nested 와
30
+ * 달리(08-class-specs §3.3), async 경계에서 SAVEPOINT 깊이 추적은 신뢰할 수 없어 **명시적 거부**가
31
+ * silent 혼선보다 안전하다. MongoDB 어댑터와 동일한 에러 코드를 쓴다.
32
+ *
33
+ * # 설정 (services.databases.<key>)
34
+ * ```js
35
+ * services: {
36
+ * databases: {
37
+ * primary: {
38
+ * driver: 'sqlite',
39
+ * filename: ':memory:', // 필수. ':memory:' = in-memory, 그 외 = 파일 경로
40
+ * readonly: false, // (옵션) 읽기 전용
41
+ * fileMustExist: false, // (옵션) 파일 없으면 throw
42
+ * timeout: 5000, // (옵션) SQLITE_BUSY 대기 ms
43
+ * wal: false, // (옵션) true → journal_mode=WAL
44
+ * pragmas: { foreign_keys: 'ON' } // (옵션) connect 후 추가 PRAGMA 적용
45
+ * },
46
+ * },
47
+ * }
48
+ * ```
49
+ *
50
+ * @module adapters/sqlite-adapter
51
+ */
52
+ import { MegaInternalError, MegaValidationError } from '../errors/http-errors.js'
53
+ import { MegaDbAdapter } from './mega-db-adapter.js'
54
+ import * as Registry from './registry.js'
55
+
56
+ /**
57
+ * @typedef {object} SqliteConfig
58
+ * @property {string} [driver] - 'sqlite' (매니저가 사용 — 어댑터는 무시).
59
+ * @property {string} filename - DB 파일 경로 또는 ':memory:' (필수).
60
+ * @property {boolean} [readonly] - 읽기 전용 연결.
61
+ * @property {boolean} [fileMustExist] - 파일이 없으면 connect 시 throw.
62
+ * @property {number} [timeout] - 잠긴 DB 대기 ms (SQLITE_BUSY 전, 기본 5000).
63
+ * @property {boolean} [wal] - true 면 connect 후 `journal_mode = WAL` 적용.
64
+ * @property {Record<string, string | number>} [pragmas] - connect 후 적용할 추가 PRAGMA 맵.
65
+ */
66
+
67
+ export class MegaSqliteAdapter extends MegaDbAdapter {
68
+ /** @type {import('better-sqlite3').Database | null} 연결된 Database 인스턴스 (connect 후에만). */
69
+ #db = null
70
+ /** @type {string} DB 파일 경로 또는 ':memory:'. */
71
+ #filename
72
+ /** @type {{ readonly: boolean, fileMustExist: boolean, timeout?: number }} */
73
+ #openOptions
74
+ /** @type {boolean} */
75
+ #wal
76
+ /** @type {Record<string, string | number>} */
77
+ #pragmas
78
+ /** @type {string | undefined} connect 후 캐시된 journal_mode (연결 동안 불변, disconnect 시 리셋). */
79
+ #journalMode
80
+
81
+ /**
82
+ * @param {SqliteConfig} [config] - services.databases.<key> 설정.
83
+ * @throws {MegaValidationError} `sqlite.filename_required` - filename 누락/빈 문자열.
84
+ * @throws {MegaValidationError} `sqlite.invalid_option` - 옵션 타입 오류.
85
+ */
86
+ constructor(config = /** @type {any} */ ({})) {
87
+ super(config)
88
+
89
+ const filename = /** @type {any} */ (config).filename
90
+ // filename 은 필수 — 빈 문자열(=better-sqlite3 의 임시 on-disk DB)도 모호하므로 명시 거부.
91
+ if (typeof filename !== 'string' || filename.length === 0) {
92
+ throw new MegaValidationError(
93
+ 'sqlite.filename_required',
94
+ "services.databases.<key>.filename is required for the sqlite driver (e.g. ':memory:' or '/path/to/app.db').",
95
+ { details: { filename: filename ?? null } },
96
+ )
97
+ }
98
+ this.#filename = filename
99
+
100
+ const { readonly = false, fileMustExist = false, timeout, wal = false, pragmas = {} } = config
101
+ // 부팅 시 fail-fast — connect 까지 미루지 않고 잘못된 옵션 타입을 생성자에서 잡는다.
102
+ this.#assertBool('readonly', readonly)
103
+ this.#assertBool('fileMustExist', fileMustExist)
104
+ this.#assertBool('wal', wal)
105
+ // better-sqlite3 는 timeout 0 이상을 허용(`timeout < 0` 만 거부)하나, 0ms 대기는 무의미하므로
106
+ // 우리는 **양의 정수만** 허용한다. 또 better-sqlite3 의 상한은 `0x7fffffff`(=2147483647)로 초과 시
107
+ // _connect() 의 RangeError 로 미뤄지므로, 부팅 fail-fast 를 위해 생성자에서 상·하한을 함께 잡는다.
108
+ // 미지정(undefined)이면 옵션 객체에서 키 자체를 뺀다(better-sqlite3 디폴트 5000ms).
109
+ if (timeout !== undefined && (!Number.isInteger(timeout) || timeout <= 0 || timeout > 0x7fffffff)) {
110
+ throw new MegaValidationError(
111
+ 'sqlite.invalid_option',
112
+ 'sqlite "timeout" must be a positive integer (ms) not greater than 2147483647.',
113
+ { details: { timeout } },
114
+ )
115
+ }
116
+ if (pragmas === null || typeof pragmas !== 'object' || Array.isArray(pragmas)) {
117
+ throw new MegaValidationError('sqlite.invalid_option', 'sqlite "pragmas" must be a plain object of { name: value }.', {
118
+ details: { type: Array.isArray(pragmas) ? 'array' : typeof pragmas },
119
+ })
120
+ }
121
+
122
+ this.#openOptions = timeout === undefined ? { readonly, fileMustExist } : { readonly, fileMustExist, timeout }
123
+ this.#wal = wal
124
+ this.#pragmas = pragmas
125
+ }
126
+
127
+ /**
128
+ * boolean 옵션 검증 헬퍼.
129
+ * @param {string} name @param {unknown} value @returns {void}
130
+ */
131
+ #assertBool(name, value) {
132
+ if (typeof value !== 'boolean') {
133
+ throw new MegaValidationError('sqlite.invalid_option', `sqlite "${name}" must be a boolean.`, {
134
+ details: { option: name, type: typeof value },
135
+ })
136
+ }
137
+ }
138
+
139
+ /**
140
+ * `better-sqlite3` Database 인스턴스 생성 + 옵션 PRAGMA 적용.
141
+ * driver 는 **connect 시점에 lazy import** — 모듈 import(배럴 경유 자기등록)만으로는
142
+ * native binding 을 로드하지 않아, 본 어댑터를 안 쓰는 환경이 better-sqlite3 설치를
143
+ * 강제받지 않는다(pg/mongo 등도 같은 패턴).
144
+ *
145
+ * @protected
146
+ * @returns {Promise<void>}
147
+ */
148
+ async _connect() {
149
+ // CJS 모듈 — ESM 동적 import 의 default 가 Database 생성자.
150
+ const { default: Database } = await import('better-sqlite3')
151
+ const db = new Database(this.#filename, this.#openOptions)
152
+ // WAL 은 동시 read/write 처리량을 높이는 표준 옵션 — 옵트인.
153
+ if (this.#wal) db.pragma('journal_mode = WAL')
154
+ for (const [name, value] of Object.entries(this.#pragmas)) {
155
+ db.pragma(`${name} = ${value}`)
156
+ }
157
+ this.#db = db
158
+ // journal_mode 는 connect 후 불변 → 한 번만 조회해 캐시. getStats() 가 매 호출마다
159
+ // PRAGMA SQL 을 실행하지 않도록(WAL 등 옵션 PRAGMA 적용 뒤의 최종값을 고정).
160
+ this.#journalMode = /** @type {any} */ (db.pragma('journal_mode', { simple: true }))
161
+ }
162
+
163
+ /**
164
+ * Database 연결 닫기. 베이스 상태 머신이 connected 상태에서만 호출을 보장한다.
165
+ * @protected
166
+ * @returns {Promise<void>}
167
+ */
168
+ async _disconnect() {
169
+ if (this.#db !== null) {
170
+ this.#db.close()
171
+ this.#db = null
172
+ // 캐시 무효화 — 다음 connect 가 새 값으로 다시 채운다(연결 끊긴 상태에선 journalMode 없음).
173
+ this.#journalMode = undefined
174
+ }
175
+ }
176
+
177
+ /**
178
+ * raw Database handle (ADR-009). `connect()` 후에만 베이스 `native` getter 가 호출한다.
179
+ * @protected
180
+ * @returns {import('better-sqlite3').Database}
181
+ */
182
+ _native() {
183
+ if (this.#db === null) {
184
+ // 베이스 native getter 가 state 검증을 먼저 하므로 정상 경로에선 도달 안 함 — 방어.
185
+ return this._notImplemented('native')
186
+ }
187
+ return this.#db
188
+ }
189
+
190
+ /**
191
+ * 헬스 체크 — 실제 `SELECT 1` 으로 응답성 확인 (베이스 디폴트는 상태만 반영).
192
+ * connect 전이거나 쿼리 실패면 `ok:false` + 사유. `/health/ready` 가 모든 어댑터 ok 를 AND.
193
+ *
194
+ * @returns {Promise<{ ok: boolean, driver: 'sqlite', state: string, filename?: string, error?: string }>}
195
+ */
196
+ async healthCheck() {
197
+ if (this.state !== 'connected' || this.#db === null) {
198
+ return { ok: false, driver: 'sqlite', state: this.state }
199
+ }
200
+ try {
201
+ const row = this.#db.prepare('SELECT 1 AS ok').get()
202
+ const ok = /** @type {any} */ (row)?.ok === 1
203
+ return { ok, driver: 'sqlite', state: this.state, filename: this.#filename }
204
+ } catch (err) {
205
+ // ping 실패는 비치명적 보고값 — healthCheck 는 throw 하지 않고 ok:false 로 표현(베이스 계약).
206
+ return {
207
+ ok: false,
208
+ driver: 'sqlite',
209
+ state: this.state,
210
+ filename: this.#filename,
211
+ error: err instanceof Error ? err.message : String(err),
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * 누적 통계 + sqlite 특화 필드.
218
+ * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, filename: string, inMemory: boolean, readonly: boolean, journalMode: string | undefined }}
219
+ */
220
+ getStats() {
221
+ return {
222
+ ...super.getStats(),
223
+ driver: 'sqlite',
224
+ filename: this.#filename,
225
+ inMemory: this.#filename === ':memory:',
226
+ readonly: this.#openOptions.readonly,
227
+ // _connect() 에서 캐시한 불변값 사용 — 미연결이면 undefined(매 호출 PRAGMA 실행 제거, L-4).
228
+ journalMode: this.#journalMode,
229
+ }
230
+ }
231
+
232
+ /**
233
+ * 명시적 트랜잭션 경계 (ADR-010, ADR-105). manual `BEGIN/COMMIT/ROLLBACK` 으로
234
+ * async `fn` 과 better-sqlite3 의 동기 트랜잭션 모델을 정합시킨다(모듈 docstring 참조).
235
+ *
236
+ * `fn` 은 **트랜잭션 컨텍스트 native handle**(= 같은 Database 인스턴스)을 인자로 받는다.
237
+ * 성공 시 COMMIT 후 `fn` 반환값을 그대로 돌려주고, `fn` 이 throw 하면 ROLLBACK 후 원본 에러를
238
+ * re-throw 한다. nested / 동시 호출은 `db.inTransaction` 으로 감지해 거부(ADR-105).
239
+ *
240
+ * hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
241
+ *
242
+ * @template T
243
+ * @param {(db: import('better-sqlite3').Database) => Promise<T> | T} fn
244
+ * @returns {Promise<T>}
245
+ * @throws {MegaInternalError} `adapter.nested_transaction_unsupported` - 이미 트랜잭션 진행 중.
246
+ */
247
+ async withTransaction(fn) {
248
+ return this._instrument('withTransaction', { table: undefined }, async () => {
249
+ const db = /** @type {import('better-sqlite3').Database} */ (this.#db)
250
+ if (db.inTransaction) {
251
+ throw new MegaInternalError(
252
+ 'adapter.nested_transaction_unsupported',
253
+ `${this.constructor.name}.withTransaction() cannot nest — a transaction is already in progress on this single SQLite connection (ADR-105).`,
254
+ { details: { adapter: this.constructor.name, filename: this.#filename } },
255
+ )
256
+ }
257
+ db.exec('BEGIN')
258
+ let result
259
+ try {
260
+ result = await fn(db)
261
+ } catch (err) {
262
+ // rollback. ROLLBACK 자체가 실패해도(드묾) 원본 에러를 가리지 않도록 격리 후 원본 re-throw.
263
+ if (db.inTransaction) {
264
+ try {
265
+ db.exec('ROLLBACK')
266
+ } catch (rollbackErr) {
267
+ // 비치명적: 원본 실패가 진짜 원인 — rollback 실패는 경고만.
268
+ console.warn(
269
+ `[MegaSqliteAdapter] ROLLBACK failed after transaction error (original error wins):`,
270
+ rollbackErr,
271
+ )
272
+ }
273
+ }
274
+ throw err
275
+ }
276
+ db.exec('COMMIT')
277
+ return result
278
+ })
279
+ }
280
+
281
+ /**
282
+ * 계측된 SQL 쿼리 (ADR-138). better-sqlite3 는 동기 API 라 `prepare` 후 reader 면 `all`(SELECT — row
283
+ * 배열), 아니면 `run`(DML — `{ changes, lastInsertRowid }`)을 쓴다. 단일 연결이라 트랜잭션 진행 중에도
284
+ * 같은 `#db` 를 쓰므로(`db.inTransaction`) 자연히 트랜잭션 안에서 실행된다. `_instrument('query', …)`
285
+ * 가 자동 span(`sqlite.query`, `db.system.name`/`db.query.text`/`mega.rows_affected`)·stats·상태 검증을 처리한다.
286
+ *
287
+ * @param {string} sql - 파라미터화된 SQL(placeholder `?` 보존).
288
+ * @param {any[]} [params] - placeholder 바인딩 값.
289
+ * @returns {Promise<any>} reader 면 row 배열, 아니면 `{ changes, lastInsertRowid }`.
290
+ */
291
+ async query(sql, params) {
292
+ return this._instrument(
293
+ 'query',
294
+ this._queryStartAttrs(sql),
295
+ async () => {
296
+ const db = /** @type {import('better-sqlite3').Database} */ (this.#db)
297
+ const stmt = db.prepare(sql)
298
+ const args = params === undefined ? [] : params
299
+ return stmt.reader ? stmt.all(...args) : stmt.run(...args)
300
+ },
301
+ // SELECT 는 row 배열 길이, DML 은 changes(영향 행 수).
302
+ (res) => ({ 'mega.rows_affected': Array.isArray(res) ? res.length : Number(res?.changes ?? 0) }),
303
+ )
304
+ }
305
+ }
306
+
307
+ // 빌트인 driver 자기등록 (ADR-044) — 배럴(`adapters/index.js`)이 본 모듈을 import 하면 트리거된다.
308
+ // better-sqlite3 native binding 은 _connect() 의 lazy import 까지 로드되지 않으므로 등록은 안전.
309
+ Registry.register('sqlite', MegaSqliteAdapter)
@@ -0,0 +1,103 @@
1
+ // @ts-check
2
+ /**
3
+ * `mega-framework/auth` — 라우트 인증·권한 가드 미들웨어 (ADR-143).
4
+ *
5
+ * 세션 기반(ADR-129) 인증 가드를 `before` 미들웨어로 제공한다. 라우트 옵션
6
+ * `{ before: [requireAuth] }` 또는 글로벌 미들웨어로 그대로 쓸 수 있다.
7
+ *
8
+ * **시그니처 호환** — 라우트 레벨 `before` 는 Fastify `preHandler` 로 `(req, reply)` 만 받고,
9
+ * 글로벌 미들웨어는 `(req, reply, ctx)` 를 받는다(ADR-134). 두 가드 모두 세션을 `req.session`
10
+ * (= `ctx.session`)에서 읽으므로 양쪽 호출 경로에서 동일하게 동작한다. 인증 결과는 `req.user` 에
11
+ * 심으며, 핸들러는 `req.user` 또는 `ctx.user`(요청당 동일 ctx, ADR-134)로 읽는다.
12
+ *
13
+ * 세션에 `userId` 가 없으면 {@link MegaAuthError}(401), 권한 부족이면 {@link MegaForbiddenError}(403)
14
+ * 를 throw 한다 — 글로벌 에러 핸들러(error-mapper)가 envelope(ADR-014)로 변환한다. silent fallback
15
+ * 없음(P4): 인증 실패는 항상 표면화된다.
16
+ *
17
+ * @module auth
18
+ */
19
+ import { MegaAuthError, MegaForbiddenError } from '../errors/http-errors.js'
20
+
21
+ /**
22
+ * 요청에서 현재 세션 객체를 꺼낸다. 라우트 `before`(ctx 없음)와 글로벌(ctx 있음) 양쪽 호환 —
23
+ * `req.session` 우선, 없으면 `ctx.session`.
24
+ * @param {import('fastify').FastifyRequest} req
25
+ * @param {Record<string, any>} [ctx]
26
+ * @returns {Record<string, any> | null}
27
+ */
28
+ function resolveSession(req, ctx) {
29
+ const fromReq = /** @type {any} */ (req)?.session
30
+ if (fromReq != null) return fromReq
31
+ return ctx?.session ?? null
32
+ }
33
+
34
+ /**
35
+ * 세션에서 인증 사용자를 확정해 `req.user` 에 심는다. 이미 `req.user` 가 있으면 그대로 둔다(멱등).
36
+ * 세션이 없거나 `userId` 가 비면 인증 실패로 간주하고 user 를 만들지 않는다.
37
+ * @param {import('fastify').FastifyRequest} req
38
+ * @param {Record<string, any>} [ctx]
39
+ * @returns {{ id: any, roles: string[] } | null}
40
+ */
41
+ function ensureUser(req, ctx) {
42
+ const existing = /** @type {any} */ (req).user
43
+ if (existing != null) return existing
44
+ const session = resolveSession(req, ctx)
45
+ const userId = session?.userId
46
+ if (userId == null) return null
47
+ const roles = Array.isArray(session?.roles) ? session.roles : []
48
+ const user = { id: userId, roles }
49
+ ;/** @type {any} */ (req).user = user
50
+ return user
51
+ }
52
+
53
+ /**
54
+ * 로그인(세션 `userId` 존재)을 요구하는 `before` 가드. 통과 시 `req.user`(`{ id, roles }`)를 채운다.
55
+ * 실패 시 401 `auth.required`.
56
+ *
57
+ * @param {import('fastify').FastifyRequest} req
58
+ * @param {import('fastify').FastifyReply} _reply
59
+ * @param {Record<string, any>} [ctx] - 글로벌 미들웨어 경로에서만 주입(라우트 before 에선 undefined).
60
+ * @returns {Promise<void>}
61
+ * @throws {MegaAuthError} 세션·userId 부재 시 401.
62
+ * @example
63
+ * router.http.get('/api/me', MeController.show, { before: [requireAuth] })
64
+ */
65
+ export async function requireAuth(req, _reply, ctx) {
66
+ const log = req.log ?? ctx?.log
67
+ const user = ensureUser(req, ctx)
68
+ if (user == null) {
69
+ log?.debug?.({ route: req.url }, 'auth.require denied — no session user')
70
+ throw new MegaAuthError('auth.required', 'Authentication required')
71
+ }
72
+ log?.debug?.({ userId: user.id }, 'auth.require ok')
73
+ }
74
+
75
+ /**
76
+ * 지정한 역할 중 하나 이상을 가진 로그인 사용자를 요구하는 `before` 가드 팩토리. 먼저 인증을
77
+ * 확정(`requireAuth` 동일 로직)한 뒤 역할 교집합을 검사한다.
78
+ *
79
+ * @param {...string} roles - 허용 역할 목록(하나라도 보유 시 통과). 최소 1개 필수.
80
+ * @returns {(req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply, ctx?: Record<string, any>) => Promise<void>}
81
+ * @throws {TypeError} 역할 인자가 비었거나 문자열이 아니면(설정 실수 fail-fast).
82
+ * @example
83
+ * router.http.post('/api/users', UsersController.create, { before: [requireRole('admin')] })
84
+ */
85
+ export function requireRole(...roles) {
86
+ if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || r.length === 0)) {
87
+ throw new TypeError('requireRole(...roles): at least one non-empty role string is required.')
88
+ }
89
+ return async function requireRoleGuard(req, _reply, ctx) {
90
+ const log = req.log ?? ctx?.log
91
+ const user = ensureUser(req, ctx)
92
+ if (user == null) {
93
+ log?.debug?.({ route: req.url, roles }, 'auth.role denied — no session user')
94
+ throw new MegaAuthError('auth.required', 'Authentication required')
95
+ }
96
+ const hasRole = user.roles.some((/** @type {string} */ r) => roles.includes(r))
97
+ if (!hasRole) {
98
+ log?.debug?.({ userId: user.id, need: roles, have: user.roles }, 'auth.role denied — missing role')
99
+ throw new MegaForbiddenError('auth.forbidden', 'Forbidden', { details: { required: roles } })
100
+ }
101
+ log?.debug?.({ userId: user.id, roles }, 'auth.role ok')
102
+ }
103
+ }
@@ -0,0 +1,56 @@
1
+ // @ts-check
2
+ /**
3
+ * `mega console` — 앱 컨텍스트를 로딩한 상태의 REPL (roadmap §338, ADR-142).
4
+ *
5
+ * `prepareRuntime`(config → 플러그인 install → 어댑터 connect → ctx, ADR-123)으로 런타임을 띄운 뒤
6
+ * Node REPL 의 전역에 `ctx`(요청 무관 boot ctx)·`config`(global config)·`mega`(host 등)를 노출한다.
7
+ * 어댑터를 직접 만져 디버깅·데이터 확인을 할 수 있다.
8
+ *
9
+ * @module cli/commands/console-cmd
10
+ */
11
+ import repl from 'node:repl'
12
+ import { prepareRuntime } from '../../core/boot.js'
13
+ import { MegaShutdown } from '../../lib/mega-shutdown.js'
14
+
15
+ /**
16
+ * 기본 REPL 팩토리 — `mega>` 프롬프트로 node REPL 시작.
17
+ * @returns {import('node:repl').REPLServer}
18
+ */
19
+ function defaultReplFactory() {
20
+ return repl.start({ prompt: 'mega> ', useGlobal: false })
21
+ }
22
+
23
+ /**
24
+ * 앱 컨텍스트를 로딩하고 REPL 을 연다.
25
+ * @param {string} projectRoot
26
+ * @param {object} [deps]
27
+ * @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
28
+ * @param {() => { context: Record<string, any>, on?: (event: string, listener: (...args: any[]) => void) => void }} [deps.replFactory] -
29
+ * 주입용(테스트). context 객체를 가진 REPL-유사 객체를 반환해야 한다('exit' 이벤트용 `on` 은 선택).
30
+ * @param {(msg: string) => void} [deps.out]
31
+ * @param {() => (Promise<void> | void)} [deps.shutdown] - 주입용(테스트). REPL 종료 시 호출하는 graceful
32
+ * shutdown 트리거. 기본 {@link MegaShutdown.now}(등록 hook 실행 후 process.exit).
33
+ * @param {(opts: { signals?: string[] }) => void} [deps.setupSignals] - 주입용(테스트). 시그널 핸들러 등록.
34
+ * 기본 {@link MegaShutdown.setupSignals}.
35
+ * @returns {Promise<{ ctx: Record<string, any>, config: object, server: { context: Record<string, any> } }>}
36
+ */
37
+ export async function startConsole(
38
+ projectRoot,
39
+ { logger, replFactory = defaultReplFactory, out = console.log, shutdown = () => MegaShutdown.now(), setupSignals = (opts) => MegaShutdown.setupSignals(opts) } = {},
40
+ ) {
41
+ const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
42
+ out('mega: console ready — globals: ctx, config, mega')
43
+ // prepareRuntime 이 어댑터/워커/wsHub 를 connect 하고 각자 MegaShutdown hook 을 자기등록한다. 정리 없이
44
+ // 두면 REPL 종료 후에도 열린 핸들(DB 풀·redis·wsHub listen)로 프로세스가 행하고 어댑터가 미정리된다.
45
+ // 따라서 SIGTERM 은 graceful shutdown 으로 받는다. SIGINT 은 REPL 이 소유(빈 줄 클리어 / 이중 입력 시
46
+ // 'exit')하므로 가로채지 않는다 — 가로채면 한 번의 Ctrl-C 가 콘솔을 죽인다(ADR-167).
47
+ setupSignals({ signals: ['SIGTERM'] })
48
+ const server = replFactory()
49
+ Object.assign(server.context, { ctx, config: global, mega: { config: global, host } })
50
+ // REPL 종료(.exit / Ctrl-D / 이중 Ctrl-C)는 'exit' 이벤트로 온다 — 이때 graceful shutdown 으로 어댑터 등을
51
+ // 정리하고 프로세스를 닫는다. 주입 fake REPL(테스트)은 `.on` 이 없을 수 있어 옵셔널 호출(ADR-167).
52
+ server.on?.('exit', () => {
53
+ void shutdown()
54
+ })
55
+ return { ctx, config: global, server }
56
+ }