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,169 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaCron — cron 표현식 파서 / 다음·이전 발생 시각 계산 (ADR-118).
4
+ *
5
+ * 검증된 공개 라이브러리 `croner`(v10) 를 우리 컨벤션으로 얇게 래핑한다(ADR-029: "내부 구현은 검증된
6
+ * 공개 npm 라이브러리를 래핑한다" — MegaRetry=p-retry, MegaCircuitBreaker=opossum 선례와 동일 원칙).
7
+ * cron 필드 파싱·범위 검증·timezone(IANA)·DST·다음 발생 시각 계산을 직접 짜면 추론 최소화 원칙 위반이
8
+ * 되기 쉽다(특히 timezone/DST 경계). croner 는 이를 **0 의존성**으로 검증된 형태로 제공한다.
9
+ *
10
+ * # cron 표현식이란 (중학생용 설명)
11
+ * "매일 새벽 3시", "5분마다" 같은 **반복 시각**을 짧은 문자열로 적는 표준 표기다. 예:
12
+ * - `0 3 * * *` → 매일 03:00 (분 시 일 월 요일)
13
+ * - `* /5 * * * *` → 5분마다 (공백 없이 `*<슬래시>5`)
14
+ * - `0 0 * * 1` → 매주 월요일 00:00
15
+ * croner 는 6필드(맨 앞에 초)도 지원한다: `* /5 * * * * *` = 5초마다.
16
+ *
17
+ * # 본 모듈은 "계산"만 — 스케줄 실행은 {@link module:lib/mega-schedule}
18
+ * MegaCron 은 표현식을 검증하고 다음/이전 발생 시각을 **계산**하는 순수 정적 유틸이다(타이머 미가동 —
19
+ * fn 없이 `new Cron(expr)` 를 만들면 croner 는 아무 타이머도 걸지 않는다, 실측 확인). 실제 시각에
20
+ * 맞춰 작업을 **실행**하는 스케줄러는 `MegaScheduler`(분산 중복방지 포함)가 담당한다.
21
+ *
22
+ * # 잘못된 표현식은 즉시 throw
23
+ * `validate`/`next`/`prev` 는 표현식이 틀리면 croner 의 파싱 에러를 **명확한 메시지로 감싸 throw**
24
+ * 한다(삼키지 않음). throw 없이 boolean 만 원하면 {@link MegaCron.isValid}.
25
+ *
26
+ * @module lib/mega-cron
27
+ * @see https://croner.56k.guru/ (croner 공식 문서)
28
+ * @see ADR-118, ADR-029 (공개 라이브러리 래핑 원칙)
29
+ */
30
+ import { Cron } from 'croner'
31
+
32
+ /**
33
+ * cron 계산 옵션. timezone 은 발생 시각 계산에 영향을 준다(예: `0 3 * * *` 를 `Asia/Seoul` 로 보면
34
+ * 한국 새벽 3시 = UTC 18:00).
35
+ *
36
+ * @typedef {Object} MegaCronOptions
37
+ * @property {string} [timezone] - IANA 타임존(예: `'Asia/Seoul'`, `'Europe/Stockholm'`). 미지정 시
38
+ * 호스트 로컬 타임존. 잘못된 타임존은 throw.
39
+ */
40
+
41
+ /**
42
+ * 표현식 문자열을 사전 검증한다. 비문자열/빈 문자열은 croner 에 넘기기 전에 더 친절한 에러로 막는다.
43
+ * @param {unknown} expr
44
+ * @returns {string}
45
+ */
46
+ function assertExprString(expr) {
47
+ if (typeof expr !== 'string' || expr.trim() === '') {
48
+ throw new TypeError(
49
+ `MegaCron: cron expression must be a non-empty string (got: ${expr === '' ? "''" : typeof expr}).`,
50
+ )
51
+ }
52
+ return expr
53
+ }
54
+
55
+ /**
56
+ * 검증·계산용 croner 인스턴스를 만들고 `compute(cron)` 을 실행한다. **fn 을 넘기지 않으므로 타이머가
57
+ * 걸리지 않는다**(순수 계산용). 표현식 오류는 croner 생성자에서, **타임존 오류는 croner 가 계산 시점에야**
58
+ * throw 하므로(생성자는 통과), 생성과 계산을 한 try/catch 로 묶어 모두 명확한 메시지로 재포장한다
59
+ * (wrap-rethrow — 원본은 `.cause`).
60
+ *
61
+ * @template T
62
+ * @param {string} expr @param {MegaCronOptions|undefined} opts @param {(cron: Cron) => T} compute @returns {T}
63
+ */
64
+ function withCron(expr, opts, compute) {
65
+ try {
66
+ // 2번째 인자는 옵션 객체(fn 아님) → 스케줄 미등록.
67
+ const cron = new Cron(expr, { timezone: opts?.timezone })
68
+ return compute(cron)
69
+ } catch (e) {
70
+ const reason = e instanceof Error ? e.message : String(e)
71
+ const tz = opts?.timezone ? ` (timezone='${opts.timezone}')` : ''
72
+ throw new Error(`MegaCron: invalid cron expression "${expr}"${tz}: ${reason}`, { cause: e })
73
+ }
74
+ }
75
+
76
+ /**
77
+ * cron 표현식 정적 유틸. 모든 메서드는 상태가 없다(인스턴스화 불필요).
78
+ *
79
+ * @example
80
+ * MegaCron.isValid('0 3 * * *') // true
81
+ * MegaCron.validate('nope') // throws Error
82
+ * MegaCron.next('0 3 * * *', new Date(), { timezone: 'Asia/Seoul' }) // 다음 새벽 3시(KST)
83
+ */
84
+ export class MegaCron {
85
+ /**
86
+ * 표현식을 파싱 검증한다. 유효하면 아무것도 반환하지 않고, 무효면 throw 한다(삼키지 않음).
87
+ * @param {string} expr - cron 표현식.
88
+ * @param {MegaCronOptions} [opts] - timezone 등(타임존도 함께 검증).
89
+ * @returns {void}
90
+ * @throws {TypeError} expr 이 비문자열/빈 문자열일 때.
91
+ * @throws {Error} 표현식/타임존 파싱 실패 시(원본은 `.cause`).
92
+ */
93
+ static validate(expr, opts) {
94
+ // nextRun() 을 1회 호출해 표현식 + 타임존을 모두 검증한다(타임존은 계산 시점에야 throw 됨).
95
+ withCron(assertExprString(expr), opts, (cron) => cron.nextRun())
96
+ }
97
+
98
+ /**
99
+ * 표현식 유효 여부를 throw 없이 boolean 으로 돌려준다. 사용자 입력 검증 UI 등에 쓴다.
100
+ * @param {string} expr - cron 표현식.
101
+ * @param {MegaCronOptions} [opts] - timezone 등.
102
+ * @returns {boolean} 유효하면 true.
103
+ */
104
+ static isValid(expr, opts) {
105
+ try {
106
+ MegaCron.validate(expr, opts)
107
+ return true
108
+ } catch {
109
+ // isValid 의 계약 자체가 "throw 없이 true/false" — 여기서 에러를 흡수하는 것이 정상 동작이다.
110
+ // (예외: 무시가 아니라 boolean 으로 변환해 반환하는 것이 본 메서드의 목적.)
111
+ return false
112
+ }
113
+ }
114
+
115
+ /**
116
+ * `from`(기본=현재) **이후** 첫 발생 시각을 계산한다.
117
+ * @param {string} expr - cron 표현식.
118
+ * @param {Date} [from] - 기준 시각(이 시각 이후의 다음 발생). 미지정 시 현재.
119
+ * @param {MegaCronOptions} [opts] - timezone 등.
120
+ * @returns {Date} 다음 발생 시각.
121
+ * @throws {Error} 표현식 무효 시, 또는 이후 발생이 없을 때(예: 일회성 과거 시각).
122
+ */
123
+ static next(expr, from, opts) {
124
+ const run = withCron(assertExprString(expr), opts, (cron) => cron.nextRun(from ?? undefined))
125
+ if (run === null) {
126
+ throw new Error(
127
+ `MegaCron: no upcoming run for "${expr}"${from ? ` after ${from.toISOString()}` : ''}.`,
128
+ )
129
+ }
130
+ return run
131
+ }
132
+
133
+ /**
134
+ * `from`(기본=현재) 이후 **n개**의 발생 시각을 계산한다. 모니터링/미리보기용.
135
+ * @param {string} expr - cron 표현식.
136
+ * @param {number} n - 개수(양의 정수).
137
+ * @param {Date} [from] - 기준 시각. 미지정 시 현재.
138
+ * @param {MegaCronOptions} [opts] - timezone 등.
139
+ * @returns {Date[]} 발생 시각 배열(시간 오름차순). 더 없으면 길이가 n 보다 짧을 수 있다.
140
+ * @throws {TypeError} n 이 양의 정수가 아닐 때.
141
+ * @throws {Error} 표현식 무효 시.
142
+ */
143
+ static nextRuns(expr, n, from, opts) {
144
+ if (!Number.isInteger(n) || n <= 0) {
145
+ throw new TypeError(`MegaCron.nextRuns: n must be a positive integer (got: ${n}).`)
146
+ }
147
+ return withCron(assertExprString(expr), opts, (cron) => cron.nextRuns(n, from ?? undefined))
148
+ }
149
+
150
+ /**
151
+ * `from`(기본=현재) **이전** 가장 가까운 발생 시각을 계산한다(역산). croner `previousRuns(1, ref)` 위임.
152
+ * @param {string} expr - cron 표현식.
153
+ * @param {Date} [from] - 기준 시각(이 시각 이전의 가장 최근 발생). 미지정 시 현재.
154
+ * @param {MegaCronOptions} [opts] - timezone 등.
155
+ * @returns {Date} 이전 발생 시각.
156
+ * @throws {Error} 표현식 무효 시, 또는 이전 발생이 없을 때.
157
+ */
158
+ static prev(expr, from, opts) {
159
+ const runs = withCron(assertExprString(expr), opts, (cron) =>
160
+ cron.previousRuns(1, from ?? undefined),
161
+ )
162
+ if (runs.length === 0) {
163
+ throw new Error(
164
+ `MegaCron: no previous run for "${expr}"${from ? ` before ${from.toISOString()}` : ''}.`,
165
+ )
166
+ }
167
+ return runs[0]
168
+ }
169
+ }
@@ -0,0 +1,179 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaHash — 비밀번호 해싱 유틸 (ADR-130 / ADR-050 개정).
4
+ *
5
+ * # 왜 해싱인가
6
+ * 비밀번호를 그대로 DB 에 저장하면 DB 가 털렸을 때 모든 비밀번호가 즉시 유출된다. 그래서 비밀번호는
7
+ * **되돌릴 수 없게** 변환(해시)해 저장하고, 로그인 때는 입력값을 똑같이 변환해 저장된 값과 **비교**만
8
+ * 한다. 공격자가 해시를 손에 넣어도 원본 비밀번호를 알아내려면 무수히 많은 후보를 일일이 해시해
9
+ * 맞춰봐야 한다(brute force). 좋은 비밀번호 해시는 이 "맞춰보기" 를 **일부러 느리고 메모리를 많이
10
+ * 먹게**(memory-hard) 만들어 GPU·ASIC 공격을 비싸게 한다.
11
+ *
12
+ * # 알고리즘 = scrypt (zero-dep, node:crypto 빌트인)
13
+ * ADR-050 은 본래 argon2id 를 정본으로 정했으나, argon2 npm 은 **네이티브 C++ 빌드 의존성**(node-gyp)
14
+ * 이라 프로젝트 zero-dep 정책과 충돌한다. 그래서 **scrypt** 채택 — Node 빌트인이라 신규
15
+ * 의존성 0 이고, OWASP 가 argon2id 다음으로 권장하는 memory-hard 함수다(ADR-130 이 ADR-050 supersede).
16
+ *
17
+ * # 파라미터(자체 판단 — ADR-130)
18
+ * OWASP(2023) 는 scrypt 최소 `N=2^17, r=8, p=1`(≈128 MiB/해시)을 권장하나, 그 메모리를 **요청마다**
19
+ * 할당하면 동시 로그인 폭주 시 서버 자체가 OOM 위험(자기-DoS)에 노출된다. 따라서 디폴트는 한 단계 낮춘
20
+ * **`N=2^15(32768), r=8, p=1`(≈32 MiB/해시)** — bcrypt 의 실효 강도를 크게 웃돌면서 서버 메모리도
21
+ * 안전한 균형점이다. 파라미터는 해시 문자열에 **임베드**되므로(아래 포맷) 나중에 N 을 올려도 기존 해시는
22
+ * 그대로 검증된다(점진적 업그레이드 가능). 더 강한 보호가 필요하면 {@link MegaHash.password.hash} 의
23
+ * `opts` 로 호출부에서 올린다.
24
+ *
25
+ * # 해시 포맷 (self-describing, PHC 풍)
26
+ * `$scrypt$N=<N>,r=<r>,p=<p>$<salt-base64url>$<hash-base64url>`
27
+ * - 알고리즘·파라미터·salt 가 전부 문자열 안에 있어, `verify` 는 별도 정보 없이 검증 가능.
28
+ * - salt 는 16바이트 random(해시마다 고유 — rainbow table 무력화), 출력 키는 32바이트.
29
+ *
30
+ * # verify 의 fail-closed 의미
31
+ * `verify(plain, hash)` 는 **boolean** 을 반환한다(03-api-spec §765). 포맷이 깨졌거나 우리 `$scrypt$`
32
+ * 해시가 아니면 **false**(= 불일치)로 처리한다 — 이는 에러를 "묵는" 게 아니라 정의된 의미다(잘못된
33
+ * 저장 해시로 로그인을 **통과시키지 않는다** = fail-closed, 보안상 안전한 방향). 비교는 항상
34
+ * `timingSafeEqual` 로 상수 시간(타이밍 공격 회피).
35
+ *
36
+ * @module lib/mega-hash
37
+ * @see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html (OWASP)
38
+ * @see https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options (node:crypto scrypt)
39
+ */
40
+ import { randomBytes, scrypt as scryptCb, timingSafeEqual } from 'node:crypto'
41
+ import { MegaValidationError } from '../errors/http-errors.js'
42
+ import { MegaConfigError } from '../errors/config-error.js'
43
+
44
+ /**
45
+ * parseHash 가 허용하는 scrypt 파라미터 방어적 상한(L-1). 손상되거나 악의적으로 조작된 저장 해시가
46
+ * 거대한 N(예: 2^30)을 담고 있으면, verify 가 그 값으로 scrypt 를 돌리며 수 GB 를 할당해 서버가
47
+ * OOM(자기-DoS)에 빠질 수 있다. 정상 운영 범위를 한참 웃도는 값은 "검증 거부"로 끊는다.
48
+ * - N <= 2^20 (≈1M, V 배열 메모리 ~1GB 한계)
49
+ * - r <= 32, p <= 16
50
+ */
51
+ const MAX_PARAMS = Object.freeze({ N: 2 ** 20, r: 32, p: 16 })
52
+
53
+ /** scrypt 디폴트 파라미터(ADR-130 — 서버 메모리 안전 균형). 해시 문자열에 임베드돼 검증 시 복원된다. */
54
+ const DEFAULTS = Object.freeze({ N: 32768, r: 8, p: 1, keylen: 32, saltBytes: 16 })
55
+
56
+ /**
57
+ * scrypt 가 필요로 하는 메모리(maxmem) 산정 — 지배항은 V 배열 `128*N*r` 이고, p 는 V 배열이 아니라
58
+ * B 버퍼(`128*r*p`)에만 곱해진다(즉 `128*N*r` 가 아니라 `128*N*r*p` 로 오해하면 안 됨, I-2).
59
+ * V 배열 + 작업 버퍼 여유까지 덮도록 `128*N*r` 의 2배 + B 버퍼(`128*r*p`) + 1MiB 여유를 둔다(파라미터 무관하게 안전).
60
+ * @param {number} N @param {number} r @param {number} p @returns {number}
61
+ */
62
+ function maxmemFor(N, r, p) {
63
+ return 128 * r * N * 2 + 128 * r * p + 1024 * 1024
64
+ }
65
+
66
+ /**
67
+ * scrypt 를 Promise 로 감싼다(이벤트 루프 블로킹 회피 — 동기 scryptSync 대신 비동기 콜백 사용).
68
+ * @param {string} plain @param {Buffer} salt @param {number} keylen
69
+ * @param {{ N: number, r: number, p: number }} params
70
+ * @returns {Promise<Buffer>}
71
+ */
72
+ function scryptAsync(plain, salt, keylen, { N, r, p }) {
73
+ return new Promise((resolve, reject) => {
74
+ scryptCb(plain, salt, keylen, { N, r, p, maxmem: maxmemFor(N, r, p) }, (err, derived) => {
75
+ if (err) reject(err)
76
+ else resolve(derived)
77
+ })
78
+ })
79
+ }
80
+
81
+ /**
82
+ * scrypt 해시 문자열을 파싱한다. 우리 포맷이 아니거나 손상되면 `null`(verify 가 false 로 귀결).
83
+ * 단, 파라미터가 방어적 상한({@link MAX_PARAMS})을 넘으면 `MegaConfigError('hash.invalid_params')`
84
+ * 를 throw 한다(L-1) — verify 가 잡아서 false 로 처리(거대 N OOM 차단, 의미상 fail-closed 동일).
85
+ * @param {string} stored
86
+ * @returns {{ N: number, r: number, p: number, salt: Buffer, hash: Buffer } | null}
87
+ * @throws {MegaConfigError} `hash.invalid_params` - N/r/p 가 안전 상한 초과.
88
+ */
89
+ function parseHash(stored) {
90
+ if (typeof stored !== 'string') return null
91
+ // `$scrypt$N=..,r=..,p=..$<salt>$<hash>` — 정확히 4개 세그먼트(빈 첫 토큰 제외).
92
+ const parts = stored.split('$')
93
+ if (parts.length !== 5 || parts[0] !== '' || parts[1] !== 'scrypt') return null
94
+ const m = /^N=(\d+),r=(\d+),p=(\d+)$/.exec(parts[2])
95
+ if (!m) return null
96
+ const N = Number(m[1])
97
+ const r = Number(m[2])
98
+ const p = Number(m[3])
99
+ // 파라미터가 양의 정수이고 N 이 2의 거듭제곱(scrypt 요구)인지 — 아니면 손상으로 간주.
100
+ if (!isPositiveInt(N) || !isPositiveInt(r) || !isPositiveInt(p) || (N & (N - 1)) !== 0) return null
101
+ // 방어적 상한 초과(L-1) — 거대 N 으로 scrypt 가 수 GB 를 할당해 서버를 OOM 시키는 자기-DoS 차단.
102
+ if (N > MAX_PARAMS.N || r > MAX_PARAMS.r || p > MAX_PARAMS.p) {
103
+ throw new MegaConfigError('hash.invalid_params', `parseHash: scrypt params exceed safe bounds (N=${N}<=${MAX_PARAMS.N}, r=${r}<=${MAX_PARAMS.r}, p=${p}<=${MAX_PARAMS.p}).`, {
104
+ details: { N, r, p, max: MAX_PARAMS },
105
+ })
106
+ }
107
+ try {
108
+ const salt = Buffer.from(parts[3], 'base64url')
109
+ const hash = Buffer.from(parts[4], 'base64url')
110
+ if (salt.length === 0 || hash.length === 0) return null
111
+ return { N, r, p, salt, hash }
112
+ } catch {
113
+ // base64url 디코딩 실패 = 손상된 해시 → null(verify false). 원본 비밀번호 leak 없음.
114
+ return null
115
+ }
116
+ }
117
+
118
+ /** @param {unknown} n @returns {boolean} 양의 정수인지(Boolean 컨벤션). */
119
+ function isPositiveInt(n) {
120
+ return typeof n === 'number' && Number.isInteger(n) && n > 0
121
+ }
122
+
123
+ /**
124
+ * MegaHash — 비밀번호 해싱 진입점. 03-api-spec §765 의 `static password = { hash, verify }` 표면.
125
+ */
126
+ export class MegaHash {
127
+ static password = {
128
+ /**
129
+ * 비밀번호를 scrypt 로 해시한다. 매 호출 새 random salt → 같은 비밀번호도 매번 다른 해시.
130
+ *
131
+ * @param {string} plain - 평문 비밀번호.
132
+ * @param {{ N?: number, r?: number, p?: number, keylen?: number, saltBytes?: number }} [opts]
133
+ * - scrypt 파라미터 오버라이드(미지정 시 ADR-130 디폴트). 강한 보호가 필요할 때만 사용.
134
+ * @returns {Promise<string>} `$scrypt$N=..,r=..,p=..$<salt>$<hash>` 포맷 문자열.
135
+ * @throws {MegaValidationError} `hash.invalid_password` - plain 이 비-문자열/빈 문자열.
136
+ */
137
+ async hash(plain, opts = {}) {
138
+ if (typeof plain !== 'string' || plain.length === 0) {
139
+ // 빈/비문자열 비밀번호는 프로그래밍 오류 — silent 진행 대신 fail-fast.
140
+ throw new MegaValidationError('hash.invalid_password', 'MegaHash.password.hash: plain password must be a non-empty string.')
141
+ }
142
+ const N = opts.N ?? DEFAULTS.N
143
+ const r = opts.r ?? DEFAULTS.r
144
+ const p = opts.p ?? DEFAULTS.p
145
+ const keylen = opts.keylen ?? DEFAULTS.keylen
146
+ const saltBytes = opts.saltBytes ?? DEFAULTS.saltBytes
147
+ const salt = randomBytes(saltBytes)
148
+ const derived = await scryptAsync(plain, salt, keylen, { N, r, p })
149
+ return `$scrypt$N=${N},r=${r},p=${p}$${salt.toString('base64url')}$${derived.toString('base64url')}`
150
+ },
151
+
152
+ /**
153
+ * 평문이 저장된 해시와 일치하는지 검증한다(상수 시간 비교). 03-api-spec §765 — verify 는 검증 액션
154
+ * 자체라 `is*` 접두사 룰의 예외로 동사형 그대로 둔다.
155
+ *
156
+ * @param {string} plain - 평문 비밀번호.
157
+ * @param {string} stored - {@link MegaHash.password.hash} 가 만든 해시 문자열.
158
+ * @returns {Promise<boolean>} 일치하면 true. 포맷 손상/비-scrypt/불일치는 모두 false(fail-closed).
159
+ */
160
+ async verify(plain, stored) {
161
+ if (typeof plain !== 'string' || plain.length === 0) return false // 빈 입력은 어떤 해시와도 불일치.
162
+ let parsed
163
+ try {
164
+ parsed = parseHash(stored)
165
+ } catch (e) {
166
+ // 상한 초과 해시(L-1)는 검증 거부 = false(fail-closed). 묵시 무시 아니라 정의된 의미 —
167
+ // 거대 N scrypt 를 돌리지 않아 OOM 차단. 그 외 에러는 가리지 않고 그대로 전파.
168
+ if (e instanceof MegaConfigError) return false
169
+ throw e
170
+ }
171
+ if (parsed === null) return false // 우리 해시가 아니거나 손상 → 불일치(통과시키지 않음).
172
+ const { N, r, p, salt, hash } = parsed
173
+ const derived = await scryptAsync(plain, salt, hash.length, { N, r, p })
174
+ // 길이가 다르면 timingSafeEqual 이 throw — 먼저 길이 비교(불일치 = false).
175
+ if (derived.length !== hash.length) return false
176
+ return timingSafeEqual(derived, hash)
177
+ },
178
+ }
179
+ }
@@ -0,0 +1,91 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * MegaHealth — 사용자 정의 헬스 체크 등록 + checkAll + isReady.
5
+ *
6
+ * 사용:
7
+ * MegaHealth.register('db', async () => ({ ok: await db.ping() }))
8
+ * MegaHealth.register('cache', async () => ({ ok: await redis.ping() }))
9
+ *
10
+ * const snapshot = await MegaHealth.checkAll() // { ok: boolean, checks: {...} }
11
+ * const ready = await MegaHealth.isReady() // boolean
12
+ *
13
+ * Fastify 통합:
14
+ * /health (liveness) — 항상 200 OK + uptime
15
+ * /health/ready — checkAll 결과 — 모두 ok 면 200, 아니면 503
16
+ *
17
+ * 셋 다 rate-limit·ASP·CSRF 면제.
18
+ */
19
+
20
+ import { MegaShutdown } from './mega-shutdown.js'
21
+
22
+ const checks = new Map()
23
+
24
+ /**
25
+ * 헬스 체크 등록.
26
+ * @param {string} name
27
+ * @param {() => Promise<{ ok: boolean, [key: string]: any }> | { ok: boolean }} fn
28
+ */
29
+ export function register(name, fn) {
30
+ if (typeof name !== 'string' || name.length === 0) {
31
+ throw new Error('MegaHealth.register: name is required (string)')
32
+ }
33
+ if (typeof fn !== 'function') {
34
+ throw new Error('MegaHealth.register: fn must be a function')
35
+ }
36
+ checks.set(name, fn)
37
+ }
38
+
39
+ /**
40
+ * 모든 체크 실행 (병렬). 하나라도 false 면 전체 ok=false.
41
+ * @returns {Promise<{ ok: boolean, checks: Record<string, { ok: boolean, error?: string, [key: string]: any }> }>}
42
+ */
43
+ export async function checkAll() {
44
+ // shutdown 중이면 readiness false (LB 가 트래픽 회수)
45
+ if (MegaShutdown.isShuttingDown()) {
46
+ return { ok: false, checks: { _shutdown: { ok: false, reason: 'shutting_down' } } }
47
+ }
48
+
49
+ const entries = [...checks.entries()]
50
+ const results = await Promise.all(
51
+ entries.map(async ([name, fn]) => {
52
+ try {
53
+ const result = await fn()
54
+ return [name, result?.ok === true ? result : { ok: false, ...result }]
55
+ } catch (err) {
56
+ return [name, { ok: false, error: err?.message ?? String(err) }]
57
+ }
58
+ }),
59
+ )
60
+ /** @type {Record<string, any>} */
61
+ const summary = {}
62
+ let allOk = true
63
+ for (const [name, r] of results) {
64
+ summary[name] = r
65
+ if (!r.ok) allOk = false
66
+ }
67
+ return { ok: allOk, checks: summary }
68
+ }
69
+
70
+ /**
71
+ * @returns {Promise<boolean>}
72
+ */
73
+ export async function isReady() {
74
+ const { ok } = await checkAll()
75
+ return ok
76
+ }
77
+
78
+ /**
79
+ * 디버그·테스트용.
80
+ * @returns {number}
81
+ */
82
+ export function registeredCount() {
83
+ return checks.size
84
+ }
85
+
86
+ /**
87
+ * 테스트용 reset.
88
+ */
89
+ export function _reset() {
90
+ checks.clear()
91
+ }