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,113 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * ESLint rule: mega/no-direct-model-import
5
+ *
6
+ * ADR-022 절대 룰 강제. 라우트·컨트롤러는 모델을 직접 import 할 수 없다.
7
+ * 항상 서비스(MegaService) 경유.
8
+ *
9
+ * 적용 대상: 파일 경로가 `apps/<any>/routes/**` 또는 `apps/<any>/controllers/**`.
10
+ * 금지 패턴:
11
+ * - 상대 경로 import 가 `models/` 디렉토리로 해석되는 경우 (`../models/user.js`,
12
+ * `../../models/posts/comment.js` 등)
13
+ * - 절대 경로 import 가 `apps/<any>/models/` 또는 `shared/models/` 를 포함하는 경우
14
+ *
15
+ * 위반 시 error.
16
+ *
17
+ * 사용 예 (eslint.config.js):
18
+ * import megaPlugin from './src/eslint-plugin/index.js'
19
+ * export default [
20
+ * ...
21
+ * { plugins: { mega: megaPlugin }, rules: { 'mega/no-direct-model-import': 'error' } },
22
+ * ]
23
+ */
24
+
25
+ /**
26
+ * 파일 경로가 routes 또는 controllers 인지 검사.
27
+ * @param {string | null | undefined} filename
28
+ * @returns {boolean}
29
+ */
30
+ function isControllerOrRouteFile(filename) {
31
+ if (!filename) return false
32
+ // posix·win 모두 지원 — 경로 구분자 정규화
33
+ const norm = filename.replace(/\\/g, '/')
34
+ return /\/apps\/[^/]+\/(routes|controllers)\//.test(norm)
35
+ }
36
+
37
+ /**
38
+ * import 경로가 models 폴더를 가리키는지 검사.
39
+ * @param {unknown} importSource - AST literal 값 (string 이 아니면 false).
40
+ * @returns {boolean}
41
+ */
42
+ function looksLikeModelImport(importSource) {
43
+ if (typeof importSource !== 'string') return false
44
+ const norm = importSource.replace(/\\/g, '/')
45
+ // 상대: ../models/, ../../models/, ./models/ 등
46
+ if (/(?:^|\/)\.\.?\/(?:[^/]+\/)*models(?:\/|$)/.test(norm)) return true
47
+ // 절대 또는 alias: apps/<name>/models/, shared/models/
48
+ if (/(?:^|\/)apps\/[^/]+\/models(?:\/|$)/.test(norm)) return true
49
+ if (/(?:^|\/)shared\/models(?:\/|$)/.test(norm)) return true
50
+ return false
51
+ }
52
+
53
+ /** @type {import('eslint').Rule.RuleModule} */
54
+ const rule = {
55
+ meta: {
56
+ type: 'problem',
57
+ docs: {
58
+ description:
59
+ 'apps/*/routes/** 와 apps/*/controllers/** 에서 models 직접 import 금지 (ADR-022)',
60
+ },
61
+ schema: [],
62
+ messages: {
63
+ direct: `라우트/컨트롤러는 모델을 직접 import 할 수 없습니다 — 서비스 경유 필수 (ADR-022). 위반 import: '{{source}}'`,
64
+ },
65
+ },
66
+
67
+ create(context) {
68
+ const filename = context.filename ?? context.getFilename?.() ?? ''
69
+ if (!isControllerOrRouteFile(filename)) return {}
70
+
71
+ /**
72
+ * @param {import('eslint').Rule.Node} node - 보고 대상 AST 노드.
73
+ * @param {unknown} sourceValue - import 소스 리터럴 값.
74
+ */
75
+ function checkImport(node, sourceValue) {
76
+ if (looksLikeModelImport(sourceValue)) {
77
+ context.report({
78
+ node,
79
+ messageId: 'direct',
80
+ data: { source: sourceValue },
81
+ })
82
+ }
83
+ }
84
+
85
+ return {
86
+ ImportDeclaration(node) {
87
+ checkImport(node, node.source?.value)
88
+ },
89
+ // CommonJS 호환 — require('...')
90
+ CallExpression(node) {
91
+ if (
92
+ node.callee &&
93
+ node.callee.type === 'Identifier' &&
94
+ node.callee.name === 'require' &&
95
+ node.arguments.length === 1 &&
96
+ node.arguments[0].type === 'Literal' &&
97
+ typeof node.arguments[0].value === 'string'
98
+ ) {
99
+ checkImport(node, node.arguments[0].value)
100
+ }
101
+ },
102
+ // dynamic import('...')
103
+ ImportExpression(node) {
104
+ if (node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
105
+ checkImport(node, node.source.value)
106
+ }
107
+ },
108
+ }
109
+ },
110
+ }
111
+
112
+ export default rule
113
+ export { isControllerOrRouteFile, looksLikeModelImport }
package/src/index.js ADDED
@@ -0,0 +1,131 @@
1
+ // MEGA-FRAMEWORK — 공개 entry point
2
+ export { MegaApp, MegaServer, Router, loadAndValidateConfig, loadRoutes } from './core/index.js'
3
+ // 중앙 부팅 orchestrator + CLI (ADR-123)
4
+ export { bootApp, buildBootContext } from './core/index.js'
5
+ export { runCli, parseArgs, runWorkerHost, runSchedulerHost, dispatchPluginCommand, USAGE } from './cli/index.js'
6
+ export { MegaService } from './core/index.js'
7
+ export { MegaCluster } from './core/index.js'
8
+ export { wrapEnvelope, errorEnvelope, buildErrorHandler, ajvErrorToValidationError } from './core/index.js'
9
+ // WS envelope + 컨트롤러 베이스 (ADR-015 / ADR-074)
10
+ export {
11
+ MegaWebSocketController,
12
+ createWsMessage,
13
+ validateWsMessage,
14
+ parseWsMessage,
15
+ generateMessageId,
16
+ WS_MESSAGE_SCHEMA,
17
+ WS_PROTOCOL_VERSION,
18
+ WS_TYPE_PATTERN,
19
+ } from './core/index.js'
20
+ export { MegaError } from './errors/mega-error.js'
21
+ export { MegaConfigError } from './errors/config-error.js'
22
+ export { MegaRouteError } from './core/router.js'
23
+ export {
24
+ MegaHttpError,
25
+ MegaValidationError,
26
+ MegaAuthError,
27
+ MegaForbiddenError,
28
+ MegaNotFoundError,
29
+ MegaConflictError,
30
+ MegaInternalError,
31
+ } from './errors/http-errors.js'
32
+ export * as MegaHealth from './lib/mega-health.js'
33
+ export { MegaShutdown } from './lib/mega-shutdown.js'
34
+ // pino + multi-sink 로거 (ADR-023/141)
35
+ export { buildLogger, buildLoggerOptions, buildTargets } from './lib/mega-logger.js'
36
+ // 재시도 백오프 유틸 (p-retry 래퍼, ADR-029/098)
37
+ export { MegaRetry, withRetry, RetryAbortError } from './lib/mega-retry.js'
38
+ // 서킷 브레이커 — 외부 호출 보호 (opossum 래퍼, ADR-029/117)
39
+ export {
40
+ MegaCircuitBreaker,
41
+ wrap as wrapCircuitBreaker,
42
+ OPEN_CIRCUIT_ERROR_CODE,
43
+ TIMEOUT_ERROR_CODE,
44
+ CAPACITY_ERROR_CODE,
45
+ } from './lib/mega-circuit-breaker.js'
46
+ // cron 파서 + cron 기반 스케줄러(분산 중복방지) (croner 래퍼, ADR-029/118)
47
+ export { MegaCron } from './lib/mega-cron.js'
48
+ export { MegaSchedule, MegaScheduler } from './lib/mega-schedule.js'
49
+ // NATS JetStream 잡 큐 — 영속 큐 + 재시도 + DLQ (JetStream 래퍼, ADR-029/119)
50
+ export { MegaJob, resolveJobRetryConfig, JOB_RETRY_DEFAULTS } from './lib/mega-job.js'
51
+ export { MegaJobQueue } from './lib/mega-job-queue.js'
52
+ // 잡 소비 워커 런타임 — 잡 등록 + bus 배선 + consume 라이프사이클 (ADR-120)
53
+ export { MegaJobWorker } from './lib/mega-job-worker.js'
54
+ // CPU 워커 풀(정본) — worker_threads/child_process 격리 + ctx.workers.<name>.run(task) (ADR-121/124)
55
+ export { MegaWorker } from './lib/mega-worker.js'
56
+ // 인증 보안 묶음 — 비밀번호 해싱(scrypt) + brute-force 차단 (ADR-049/050/130)
57
+ export { MegaHash } from './lib/mega-hash.js'
58
+ export { MegaBruteForce } from './lib/mega-brute-force.js'
59
+ // 서버사이드 템플릿(정본) — EJS + ejs-mate 렌더 (ADR-011/136)
60
+ export { MegaTemplate } from './core/template.js'
61
+ // 정적 자산 옵트인 — @fastify/static (ADR-071/139)
62
+ export { registerStaticAssets, normalizeStaticAssets } from './core/static-assets.js'
63
+ // OpenAPI/Swagger 옵트인 — @fastify/swagger + swagger-ui (ADR-070/140)
64
+ export { registerOpenapi, normalizeOpenapi } from './core/openapi.js'
65
+ // 플러그인 시스템 — install(mega) 패턴 + apiVersion 호환 (ADR-079/122)
66
+ export { MegaPluginHost, loadPlugins, CORE_API_VERSION } from './lib/mega-plugin.js'
67
+ // .env → 어댑터 옵션 자동 매핑 (12-factor, ADR-109)
68
+ export { buildAdapterEnvConfig } from './lib/env-mapper.js'
69
+ // OpenTelemetry 분산 트레이싱 옵트인 (ADR-077 hook 위 자동 span, ADR-114)
70
+ export * as MegaTracing from './lib/mega-tracing.js'
71
+ // OpenTelemetry 메트릭 + Prometheus /metrics 옵트인 (ADR-072/131)
72
+ export * as MegaMetrics from './lib/mega-metrics.js'
73
+ // 클러스터 메트릭 집계 — 워커별 메트릭을 마스터 IPC 로 합산 (ADR-154/163)
74
+ export { collectCluster, mergeExposition } from './core/cluster-metrics.js'
75
+
76
+ // 데이터 어댑터 베이스 트리 + 드라이버 레지스트리 (ADR-027/044/045/077)
77
+ export {
78
+ MegaAdapter,
79
+ MegaDbAdapter,
80
+ MegaCacheAdapter,
81
+ MegaBusAdapter,
82
+ MegaLockAdapter,
83
+ MegaLogSinkAdapter,
84
+ MegaSessionAdapter,
85
+ MegaAdapterRegistry,
86
+ BUILTIN_DRIVERS,
87
+ MegaAdapterManager,
88
+ MegaSqliteAdapter,
89
+ MegaPostgresAdapter,
90
+ MegaMariaAdapter,
91
+ MegaMongoAdapter,
92
+ MegaRedisAdapter,
93
+ MegaFileAdapter,
94
+ MegaNatsAdapter,
95
+ MegaRedlockAdapter,
96
+ MegaFileSessionAdapter,
97
+ MegaRedisSessionAdapter,
98
+ } from './adapters/index.js'
99
+ // 요청 ctx 의 db/cache/bus 함수형 lookup 배선 (ADR-102)
100
+ export { buildHttpCtx, buildAdapterAccessors } from './core/index.js'
101
+
102
+ // 데이터 모델 베이스 (ADR-008/009/010/081)
103
+ export { MegaModel } from './models/index.js'
104
+
105
+ // 보안 플러그인 자동 등록 — helmet/cors/rate-limit/csrf + ASP HTTP (ADR-127)
106
+ export { registerSecurityPlugins, DEFAULT_RATE_LIMIT } from './core/security.js'
107
+ // 세션 — 미들웨어 + 스토어 팩토리 + cleanup 스케줄 + 어댑터 (ADR-129/046)
108
+ export { registerSession, generateSid, readSession, createSessionStore, SESSION_STORE_DRIVERS, createSessionCleanupSchedule } from './core/index.js'
109
+
110
+ // ASP (Application Secure Protocol) — 서버측 HTTP/WS terminator (ADR-053 ~ ADR-060, ADR-094)
111
+ export { registerAspPlugin } from './lib/asp/plugin.js'
112
+ export { MegaAspTerminator } from './lib/asp/ws-terminator.js'
113
+ export { MegaAspNonceCache, MegaMemoryNonceStore } from './lib/asp/nonce-cache.js'
114
+ export { MegaAspDecryptError, ASP_RULES } from './lib/asp/errors.js'
115
+ export { normalizeAspConfig } from './lib/asp/config.js'
116
+
117
+ // Bridge ↔ Hub 12-타입 프로토콜 (ADR-033/059/097)
118
+ export { MegaWsHub, DEFAULT_HEARTBEAT_MS, runWsHubCli } from './cli/ws-hub.js'
119
+ export { MegaHubLink } from './core/hub-link.js'
120
+ // WS per-message deflate 압축 (ADR-078)
121
+ export { buildPerMessageDeflate, checkCompressionConfig, COMPRESSION_DEFAULTS } from './core/ws-compression.js'
122
+ export {
123
+ HUB_MESSAGE_TYPES,
124
+ HUB_TYPE_SET,
125
+ HUB_PAYLOAD_SCHEMAS,
126
+ HUB_CLOSE_CODES,
127
+ CLOSE_CODE_DRAIN,
128
+ validateHubMessage,
129
+ createHubMessage,
130
+ parseHubMessage,
131
+ } from './lib/hub-protocol.js'
@@ -0,0 +1,83 @@
1
+ // @ts-check
2
+ /**
3
+ * ASP 설정 정규화 + fail-fast 검증 (ADR-056, ADR-057, ADR-058).
4
+ *
5
+ * 부팅 시 한 번 호출되어 다음을 강제한다 (roadmap Phase 1 검증 기준):
6
+ * - `failOpen` 은 코어가 **강제로 false** 로 덮어쓴다 (사용자가 true 박아도 무시 + warn).
7
+ * - `masterSecret` 누락 → throw (ADR-056).
8
+ * - `nonceCache` 가 켜져 있으면 `cache`(또는 MegaAspNonceCache) 존재 검증.
9
+ *
10
+ * @module lib/asp/config
11
+ */
12
+ import { MegaAspNonceCache } from './nonce-cache.js'
13
+
14
+ /**
15
+ * @typedef {Object} NormalizedAspConfig
16
+ * @property {string} masterSecret
17
+ * @property {string[]} enabledPaths - glob 패턴 (예: '/api/*')
18
+ * @property {string} headerSignal - 암호화 시그널 헤더명 (디폴트 'X-Mega-Encrypted')
19
+ * @property {string} timestampHeader - 타임스탬프 헤더명 (디폴트 'X-Timestamp')
20
+ * @property {number} driftMs - 허용 timestamp drift (디폴트 60000)
21
+ * @property {false} failOpen - 항상 false (강제)
22
+ * @property {MegaAspNonceCache | null} nonceCache
23
+ */
24
+
25
+ /**
26
+ * 원시 ASP config → 정규화 + 검증.
27
+ *
28
+ * @param {Record<string, any>} asp - { masterSecret, enabledPaths?, headerSignal?, timestampHeader?,
29
+ * driftMs?, failOpen?, nonceCache? }
30
+ * @param {{ logger?: { warn: Function } }} [opts]
31
+ * @returns {NormalizedAspConfig}
32
+ * @throws {Error} masterSecret 누락 / nonceCache 잘못된 형태.
33
+ */
34
+ export function normalizeAspConfig(asp, { logger } = {}) {
35
+ if (!asp || typeof asp !== 'object') {
36
+ throw new Error('ASP config must be an object (set asp in mega.config.js).')
37
+ }
38
+
39
+ // masterSecret 필수 (ADR-056) — 누락 시 fail-fast.
40
+ if (typeof asp.masterSecret !== 'string' || asp.masterSecret.length === 0) {
41
+ throw new Error(
42
+ 'ASP config: masterSecret is required (ADR-056). Set asp.masterSecret in mega.config.js.',
43
+ )
44
+ }
45
+
46
+ // failOpen 강제 false (ADR-057). 사용자가 true 박았으면 무시 + warn.
47
+ if (asp.failOpen === true) {
48
+ const msg =
49
+ 'asp.failOpen=true is ignored — core forces fail-closed (ADR-057). ' +
50
+ 'Key mismatch always rejects; plaintext never passes.'
51
+ if (logger?.warn) logger.warn({ asp: 'failOpen', forced: false }, msg)
52
+ else console.warn(`[mega:asp] ${msg}`)
53
+ }
54
+
55
+ // nonceCache 검증 (ADR-058) — 켜져 있으면 cache 존재 필수.
56
+ let nonceCache = null
57
+ if (asp.nonceCache) {
58
+ if (asp.nonceCache instanceof MegaAspNonceCache) {
59
+ nonceCache = asp.nonceCache
60
+ } else if (asp.nonceCache.cache) {
61
+ nonceCache = new MegaAspNonceCache({
62
+ cache: asp.nonceCache.cache,
63
+ ttlSec: asp.nonceCache.ttlSec,
64
+ })
65
+ } else {
66
+ throw new Error(
67
+ 'ASP config: nonceCache requires a `cache` with setIfNotExists(key, ttlSec) ' +
68
+ 'or a MegaAspNonceCache instance (ADR-058).',
69
+ )
70
+ }
71
+ }
72
+
73
+ return {
74
+ masterSecret: asp.masterSecret,
75
+ enabledPaths: Array.isArray(asp.enabledPaths) ? asp.enabledPaths : [],
76
+ headerSignal: typeof asp.headerSignal === 'string' ? asp.headerSignal : 'X-Mega-Encrypted',
77
+ timestampHeader:
78
+ typeof asp.timestampHeader === 'string' ? asp.timestampHeader : 'X-Timestamp',
79
+ driftMs: typeof asp.driftMs === 'number' && asp.driftMs > 0 ? asp.driftMs : 60000,
80
+ failOpen: false, // 강제 (ADR-057)
81
+ nonceCache,
82
+ }
83
+ }
@@ -0,0 +1,145 @@
1
+ // MEGA ASP 알고리즘 JS mirror — Rust 정본(`packages/mega-client-wasm/src/crypto.rs`) 과 byte-level 호환.
2
+ // zero-dep: node:crypto 만 사용. known-vector 양방향 단언 (ADR-060).
3
+
4
+ import { createHmac, createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto'
5
+ import { Buffer } from 'node:buffer'
6
+
7
+ const OBFUSCATE_SEED = 0x5a
8
+ const AES_NONCE_LEN = 12
9
+ const AES_TAG_LEN = 16
10
+
11
+ /**
12
+ * SHA256(hex(HMAC-SHA256(masterSecret, "domain:path:uaSlice:timestamp"))) → 32 bytes Buffer.
13
+ */
14
+ export function deriveKey(masterSecret, domain, path, uaSlice, timestamp) {
15
+ const seed = `${domain}:${path}:${uaSlice}:${timestamp}`
16
+ const mac = createHmac('sha256', masterSecret).update(seed).digest()
17
+ const hexKey = mac.toString('hex')
18
+ return createHash('sha256').update(hexKey).digest() // Buffer length 32
19
+ }
20
+
21
+ /**
22
+ * timestamp mod UA 길이 위치에서 4 byte 순환 슬라이스. UA 비어 있으면 "0000".
23
+ * 비-ASCII byte 도 char.fromCharCode(byte) 로 매핑 — Rust char::from_u32(b as u32) 와 동일.
24
+ */
25
+ export function getUaSlice(ua, timestamp) {
26
+ const uaBuf = Buffer.from(ua, 'utf8')
27
+ if (uaBuf.length === 0) return '0000'
28
+ const uaLen = uaBuf.length
29
+ const startIdx = Number(BigInt(timestamp) % BigInt(uaLen))
30
+ let out = ''
31
+ for (let i = 0; i < 4; i++) {
32
+ const b = uaBuf[(startIdx + i) % uaLen]
33
+ out += String.fromCharCode(b)
34
+ }
35
+ return out
36
+ }
37
+
38
+ /**
39
+ * 모든 byte 에 nibble-swap + XOR(0x5A) 적용. in-place.
40
+ */
41
+ function obfuscateBytes(buf) {
42
+ for (let i = 0; i < buf.length; i++) {
43
+ const b = buf[i]
44
+ const swapped = ((b << 4) | (b >> 4)) & 0xff
45
+ buf[i] = swapped ^ OBFUSCATE_SEED
46
+ }
47
+ }
48
+
49
+ /**
50
+ * obfuscateBytes 의 역연산. byte-level 호환 위해 fuzionx 원본과 동일.
51
+ * XOR(0x5A) → nibble-swap (역순) 적용. in-place.
52
+ */
53
+ function deobfuscateBytes(buf) {
54
+ for (let i = 0; i < buf.length; i++) {
55
+ const xored = (buf[i] ^ OBFUSCATE_SEED) & 0xff
56
+ buf[i] = ((xored << 4) | (xored >> 4)) & 0xff
57
+ }
58
+ }
59
+
60
+ /**
61
+ * AES-256-GCM 암호화 → nibble-swap → base64.
62
+ * @param {Buffer} key — 32 bytes
63
+ * @param {Buffer|string} plain
64
+ * @returns {string} base64
65
+ */
66
+ export function encryptTransport(key, plain) {
67
+ const nonce = randomBytes(AES_NONCE_LEN)
68
+ const cipher = createCipheriv('aes-256-gcm', key, nonce)
69
+ const plainBuf = Buffer.isBuffer(plain) ? plain : Buffer.from(plain, 'utf8')
70
+ const ct = Buffer.concat([cipher.update(plainBuf), cipher.final()])
71
+ const tag = cipher.getAuthTag()
72
+ const combined = Buffer.concat([nonce, ct, tag]) // nonce || ciphertext || tag
73
+ obfuscateBytes(combined)
74
+ return combined.toString('base64')
75
+ }
76
+
77
+ /**
78
+ * base64 → deobfuscate → AES-256-GCM 복호화. Buffer 반환.
79
+ * @returns {Buffer}
80
+ * @throws Error
81
+ */
82
+ export function decryptTransport(key, encryptedB64) {
83
+ let s = encryptedB64.trim()
84
+ while (s.length % 4 !== 0) s += '='
85
+ const combined = Buffer.from(s, 'base64')
86
+ if (combined.length < AES_NONCE_LEN + AES_TAG_LEN) {
87
+ throw new Error('encrypted blob too short')
88
+ }
89
+ deobfuscateBytes(combined)
90
+ const nonce = combined.subarray(0, AES_NONCE_LEN)
91
+ const tag = combined.subarray(combined.length - AES_TAG_LEN)
92
+ const ct = combined.subarray(AES_NONCE_LEN, combined.length - AES_TAG_LEN)
93
+ const decipher = createDecipheriv('aes-256-gcm', key, nonce)
94
+ decipher.setAuthTag(tag)
95
+ return Buffer.concat([decipher.update(ct), decipher.final()])
96
+ }
97
+
98
+ /**
99
+ * 암호문(base64)에서 AES-GCM nonce(앞 12 byte) 만 추출 — replay 방어용 (ADR-058).
100
+ *
101
+ * deobfuscate 후 앞 12 byte 가 nonce. **복호화하지 않고** nonce 만 뽑아 nonce_cache 의
102
+ * SETNX 키로 쓴다. GCM nonce 는 암호화마다 random 이라 동일 nonce 재등장 = replay.
103
+ *
104
+ * @param {string} encryptedB64 — encryptTransport 가 만든 base64 (URL-safe 아님 — STANDARD)
105
+ * @returns {Buffer} 12-byte nonce
106
+ * @throws {Error} blob 이 nonce+tag 보다 짧으면.
107
+ */
108
+ export function extractNonce(encryptedB64) {
109
+ let s = encryptedB64.trim()
110
+ while (s.length % 4 !== 0) s += '='
111
+ const combined = Buffer.from(s, 'base64')
112
+ if (combined.length < AES_NONCE_LEN + AES_TAG_LEN) {
113
+ throw new Error('encrypted blob too short')
114
+ }
115
+ deobfuscateBytes(combined)
116
+ // subarray 는 같은 메모리 view — 복사본을 반환해 호출부가 안전하게 보관.
117
+ return Buffer.from(combined.subarray(0, AES_NONCE_LEN))
118
+ }
119
+
120
+ /**
121
+ * WS 프레임 암호화 — bare "<ts>:<base64>" 반환.
122
+ * P:/E: prefix 부착은 MegaAspTerminator 책임 (ADR-083).
123
+ */
124
+ export function wsEncrypt(masterSecret, domain, path, ua, plain) {
125
+ const ts = Date.now()
126
+ const uaSlice = getUaSlice(ua, ts)
127
+ const key = deriveKey(masterSecret, domain, path, uaSlice, ts)
128
+ const enc = encryptTransport(key, plain)
129
+ return `${ts}:${enc}`
130
+ }
131
+
132
+ /**
133
+ * WS 프레임 복호화 — bare "<ts>:<base64>" 입력. 평문 string 반환.
134
+ */
135
+ export function wsDecrypt(masterSecret, domain, path, ua, frame) {
136
+ const colonIdx = frame.indexOf(':')
137
+ if (colonIdx < 0) throw new Error("missing ':' in frame")
138
+ const tsStr = frame.slice(0, colonIdx)
139
+ const enc = frame.slice(colonIdx + 1)
140
+ const ts = Number(tsStr)
141
+ if (!Number.isFinite(ts) || ts < 0) throw new Error('invalid timestamp')
142
+ const uaSlice = getUaSlice(ua, ts)
143
+ const key = deriveKey(masterSecret, domain, path, uaSlice, ts)
144
+ return decryptTransport(key, enc).toString('utf8')
145
+ }
@@ -0,0 +1,49 @@
1
+ // @ts-check
2
+ /**
3
+ * ASP 에러 (ADR-025, ADR-053, ADR-058).
4
+ *
5
+ * MegaAspDecryptError 는 요청 복호화·검증 실패 시 던진다. 글로벌 에러 핸들러
6
+ * (error-mapper.js) 가 MegaHttpError 계층을 그대로 envelope 으로 변환하므로,
7
+ * 403 + code `asp.decrypt_failed` + details 배열(ADR-075) 이 자동으로 응답된다.
8
+ *
9
+ * @module lib/asp/errors
10
+ */
11
+ import { MegaHttpError } from '../../errors/http-errors.js'
12
+
13
+ /** ASP rule 종류 (envelope details 의 `rule` 필드). */
14
+ export const ASP_RULES = Object.freeze({
15
+ KEY_MISMATCH: 'key_mismatch', // 복호화 실패 (auth tag 불일치 / 잘못된 키)
16
+ INVALID_PAYLOAD: 'invalid_payload', // ciphertext 깨짐 / 포맷 오류
17
+ DRIFT: 'drift', // timestamp 가 허용 윈도우(±driftMs) 밖
18
+ REPLAY: 'replay', // 동일 nonce 재사용 (nonce_cache, ADR-058)
19
+ MISSING_TIMESTAMP: 'missing_timestamp', // X-Timestamp 헤더 누락
20
+ SIGNAL_REQUIRED: 'signal_required', // ASP 활성 경로인데 암호화 시그널 없음 (fail-closed)
21
+ })
22
+
23
+ /**
24
+ * ASP 요청 복호화/검증 실패 — 403. fail-closed (ADR-057): 평문 통과 금지.
25
+ *
26
+ * `details` 는 배열 표준(ADR-075): `[{ field, rule, value? }]`. 여러 ASP 실패를
27
+ * 한 응답으로 표현 가능. 단일 실패도 1-원소 배열로 통일.
28
+ */
29
+ export class MegaAspDecryptError extends MegaHttpError {
30
+ /**
31
+ * @param {string} rule - ASP_RULES 중 하나.
32
+ * @param {string} [message] - 사람이 읽는 메시지 (디버그용 — 시크릿 금지).
33
+ * @param {{ field?: string, value?: unknown, cause?: unknown }} [opts]
34
+ */
35
+ constructor(rule, message, opts = {}) {
36
+ super(403, 'asp.decrypt_failed', message ?? `ASP request rejected (${rule})`, {
37
+ details: [
38
+ {
39
+ field: opts.field ?? 'request',
40
+ rule,
41
+ ...(opts.value !== undefined ? { value: opts.value } : {}),
42
+ },
43
+ ],
44
+ cause: opts.cause,
45
+ })
46
+ /** @type {string} 실패 종류 (close code / 로깅 분기용). */
47
+ this.rule = rule
48
+ }
49
+ }
@@ -0,0 +1,94 @@
1
+ // @ts-check
2
+ /**
3
+ * ASP replay 방어 2단계 중 nonce 캐시 (ADR-058).
4
+ *
5
+ * AES-GCM 의 12-byte random nonce 를 cache 에 SETNX (set-if-not-exists) 한다.
6
+ * 동일 nonce 가 두 번째로 등장하면 replay → reject. timestamp drift(±60s) 와 결합해
7
+ * 윈도우 내 재전송까지 차단한다.
8
+ *
9
+ * cache 백엔드는 다음 계약만 만족하면 된다 (Redis SETNX 대응):
10
+ * `async setIfNotExists(key: string, ttlSec: number): Promise<boolean>`
11
+ * - 새로 set 했으면 `true`, 이미 존재하면 `false`.
12
+ *
13
+ * Redis 어댑터 도입 전이라 {@link MegaMemoryNonceStore} 를 기본 제공.
14
+ * 멀티 인스턴스 운영은 Redis 백엔드 권장 (in-memory 는 프로세스별 분리).
15
+ *
16
+ * @module lib/asp/nonce-cache
17
+ */
18
+
19
+ /** nonce_cache 키 prefix (Redis 키 컨벤션, ADR-058). */
20
+ const NONCE_KEY_PREFIX = 'asp:nonce:'
21
+
22
+ /** 기본 TTL — drift 윈도우(±60s) 의 2배 (ADR-058: EX 120). */
23
+ const DEFAULT_TTL_SEC = 120
24
+
25
+ /**
26
+ * MegaAspNonceCache — nonce SETNX 래퍼.
27
+ */
28
+ export class MegaAspNonceCache {
29
+ /**
30
+ * @param {Object} [opts]
31
+ * @param {{ setIfNotExists: (key: string, ttlSec: number) => Promise<boolean> }} [opts.cache] - 누락 시 throw.
32
+ * @param {number} [opts.ttlSec=120]
33
+ */
34
+ constructor({ cache, ttlSec = DEFAULT_TTL_SEC } = {}) {
35
+ if (!cache || typeof cache.setIfNotExists !== 'function') {
36
+ throw new Error(
37
+ 'MegaAspNonceCache: `cache` with async setIfNotExists(key, ttlSec) is required (ADR-058).',
38
+ )
39
+ }
40
+ this._cache = cache
41
+ this._ttlSec = ttlSec
42
+ }
43
+
44
+ /**
45
+ * nonce 가 처음 등장하면 기록 후 true, 이미 봤으면(=replay) false.
46
+ *
47
+ * @param {string} nonceHex - {@link import('./crypto.js').extractNonce} 결과의 hex.
48
+ * @returns {Promise<boolean>} fresh 면 true, replay 면 false.
49
+ */
50
+ async isFresh(nonceHex) {
51
+ return this._cache.setIfNotExists(`${NONCE_KEY_PREFIX}${nonceHex}`, this._ttlSec)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 인메모리 nonce 저장소 — dev / 단일 인스턴스 / 테스트용.
57
+ *
58
+ * Map<key, 만료 epoch ms>. setIfNotExists 시 만료된 항목은 자동으로 덮어쓴다.
59
+ * 멀티 인스턴스에서는 프로세스별로 분리되므로 Redis 권장 (ADR-058).
60
+ */
61
+ export class MegaMemoryNonceStore {
62
+ constructor() {
63
+ /** @type {Map<string, number>} key → 만료 epoch ms */
64
+ this._store = new Map()
65
+ }
66
+
67
+ /**
68
+ * @param {string} key
69
+ * @param {number} ttlSec
70
+ * @returns {Promise<boolean>} 새로 set 했으면 true, 유효한 기존 항목 있으면 false.
71
+ */
72
+ async setIfNotExists(key, ttlSec) {
73
+ const now = Date.now()
74
+ const exp = this._store.get(key)
75
+ if (exp !== undefined && exp > now) return false
76
+ this._store.set(key, now + ttlSec * 1000)
77
+ return true
78
+ }
79
+
80
+ /**
81
+ * 만료 항목 제거 (주기 cleanup 용 — 현재는 수동/테스트 호출).
82
+ * @param {number} [now=Date.now()]
83
+ */
84
+ evictExpired(now = Date.now()) {
85
+ for (const [k, exp] of this._store) {
86
+ if (exp <= now) this._store.delete(k)
87
+ }
88
+ }
89
+
90
+ /** 현재 저장된 항목 수 (테스트·디버그용). */
91
+ get size() {
92
+ return this._store.size
93
+ }
94
+ }