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.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 파일 로드 + 전체 검증 진입점.
|
|
4
|
+
*
|
|
5
|
+
* 프로젝트 루트에서 mega.config.js + 활성 앱(ADR-066 whitelist) 의 app.config.js 를
|
|
6
|
+
* import 하고 검증한다. 부팅 시 한 번 호출되며, 검증 실패 시 MegaConfigError 로
|
|
7
|
+
* fail-fast 한다.
|
|
8
|
+
*
|
|
9
|
+
* @module core/config-loader
|
|
10
|
+
*/
|
|
11
|
+
import { statSync } from 'node:fs'
|
|
12
|
+
import { join, resolve as pathResolve } from 'node:path'
|
|
13
|
+
import { pathToFileURL } from 'node:url'
|
|
14
|
+
import { MegaConfigError } from '../errors/config-error.js'
|
|
15
|
+
import {
|
|
16
|
+
validateGlobalConfig,
|
|
17
|
+
validateAppConfig,
|
|
18
|
+
validateHostCollisions,
|
|
19
|
+
} from './config-validator.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 프로젝트 루트에서 mega.config.js + 활성 앱 config 들 로드 + 검증.
|
|
23
|
+
* @param {string} projectRoot - 프로젝트 루트 절대 경로
|
|
24
|
+
* @returns {Promise<{global: Object, apps: Array<{name: string, config: Object}>}>}
|
|
25
|
+
* @throws {MegaConfigError}
|
|
26
|
+
*/
|
|
27
|
+
export async function loadAndValidateConfig(projectRoot) {
|
|
28
|
+
const globalPath = join(projectRoot, 'mega.config.js')
|
|
29
|
+
const global = await importConfig(globalPath, 'mega.config.js')
|
|
30
|
+
validateGlobalConfig(global)
|
|
31
|
+
|
|
32
|
+
/** @type {Array<{name: string, config: Object}>} */
|
|
33
|
+
const apps = []
|
|
34
|
+
const appNames = global.apps ?? []
|
|
35
|
+
for (const name of appNames) {
|
|
36
|
+
const folderPath = join(projectRoot, 'apps', name)
|
|
37
|
+
const appConfigPath = join(folderPath, 'app.config.js')
|
|
38
|
+
|
|
39
|
+
if (!isDirectory(folderPath)) {
|
|
40
|
+
throw new MegaConfigError(
|
|
41
|
+
'config.app_folder_missing',
|
|
42
|
+
`App '${name}' is in 'apps' array but folder 'apps/${name}' not found.`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
if (!isFile(appConfigPath)) {
|
|
46
|
+
throw new MegaConfigError(
|
|
47
|
+
'config.app_config_missing',
|
|
48
|
+
`App '${name}' is in 'apps' array but 'apps/${name}/app.config.js' not found.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config = await importConfig(appConfigPath, `apps/${name}/app.config.js`)
|
|
53
|
+
validateAppConfig(config, name, global)
|
|
54
|
+
apps.push({ name, config })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validateHostCollisions(apps)
|
|
58
|
+
|
|
59
|
+
return { global, apps }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* config 파일을 동적 import 하고 default export 를 반환.
|
|
64
|
+
* @param {string} absPath - 절대 경로
|
|
65
|
+
* @param {string} label - 에러 메시지용 라벨
|
|
66
|
+
* @returns {Promise<Record<string, any>>}
|
|
67
|
+
* @throws {MegaConfigError}
|
|
68
|
+
*/
|
|
69
|
+
async function importConfig(absPath, label) {
|
|
70
|
+
try {
|
|
71
|
+
const mod = await import(pathToFileURL(pathResolve(absPath)).href)
|
|
72
|
+
if (!mod.default) {
|
|
73
|
+
throw new MegaConfigError('config.no_default_export', `${label} must have a default export.`)
|
|
74
|
+
}
|
|
75
|
+
return mod.default
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// 우리가 던진 검증 에러는 그대로 전파 (wrap 하면 code 가 가려짐).
|
|
78
|
+
if (err instanceof MegaConfigError) throw err
|
|
79
|
+
// 그 외(문법 오류·모듈 해석 실패 등)는 원인 보존하며 wrap (패턴 2).
|
|
80
|
+
throw new MegaConfigError('config.load_failed', `Failed to load ${label}: ${err.message}`, {
|
|
81
|
+
cause: err,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 경로가 디렉토리인지 검사.
|
|
88
|
+
* @param {string} p
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function isDirectory(p) {
|
|
92
|
+
try {
|
|
93
|
+
return statSync(p).isDirectory()
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// 경로가 없으면(ENOENT) "디렉토리 아님" 이 정상 의미 — 호출부가 missing 처리.
|
|
96
|
+
// 권한 등 다른 에러는 숨기지 말고 전파 (silent catch 금지).
|
|
97
|
+
if (err.code === 'ENOENT') return false
|
|
98
|
+
throw err
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 경로가 일반 파일인지 검사.
|
|
104
|
+
* @param {string} p
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function isFile(p) {
|
|
108
|
+
try {
|
|
109
|
+
return statSync(p).isFile()
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// ENOENT 는 "파일 없음" 정상 케이스. 그 외는 전파.
|
|
112
|
+
if (err.code === 'ENOENT') return false
|
|
113
|
+
throw err
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaConfig 검증 로직.
|
|
4
|
+
*
|
|
5
|
+
* 부팅 시 한 번 실행되어 잘못된 config 를 fail-fast 로 throw 한다 (ADR-062).
|
|
6
|
+
* - 3-스코프 강제 (Global-only / App-only / Shared-Reference — ADR-061)
|
|
7
|
+
* - 알 수 없는 키 오타 감지 + Did-you-mean suggestion (ADR-062)
|
|
8
|
+
* - name ↔ 폴더명 일치 + Shared-Reference cross-check (ADR-067)
|
|
9
|
+
* - 호스트 충돌 감지 (ADR-067)
|
|
10
|
+
*
|
|
11
|
+
* @module core/config-validator
|
|
12
|
+
*/
|
|
13
|
+
import { MegaConfigError } from '../errors/config-error.js'
|
|
14
|
+
import { GLOBAL_ONLY_KEYS, APP_ONLY_KEYS, SHARED_REFERENCE_KEYS } from './scope-registry.js'
|
|
15
|
+
import { checkCompressionConfig } from './ws-compression.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 메인 config 검증.
|
|
19
|
+
* @param {Record<string, any>} globalConfig - mega.config.js export 객체
|
|
20
|
+
* @throws {MegaConfigError}
|
|
21
|
+
*/
|
|
22
|
+
export function validateGlobalConfig(globalConfig) {
|
|
23
|
+
if (!globalConfig || typeof globalConfig !== 'object') {
|
|
24
|
+
throw new MegaConfigError('config.invalid', 'mega.config.js must export an object')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 1) 알 수 없는 키 + 잘못된 스코프
|
|
28
|
+
for (const key of Object.keys(globalConfig)) {
|
|
29
|
+
if (GLOBAL_ONLY_KEYS.includes(key)) continue
|
|
30
|
+
if (APP_ONLY_KEYS.includes(key)) {
|
|
31
|
+
throw new MegaConfigError(
|
|
32
|
+
'config.wrong_scope',
|
|
33
|
+
`Global config cannot define '${key}'. Move to apps/<name>/app.config.js (ADR-061).`,
|
|
34
|
+
{ details: { key, scope: 'app-only' } },
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
// 알 수 없는 키 — 오타 의심
|
|
38
|
+
const suggestion = suggestKey(key, [...GLOBAL_ONLY_KEYS, ...APP_ONLY_KEYS])
|
|
39
|
+
throw new MegaConfigError(
|
|
40
|
+
'config.unknown_key',
|
|
41
|
+
`Unknown key '${key}' in mega.config.js.${suggestion ? ` Did you mean '${suggestion}'?` : ''}`,
|
|
42
|
+
{ details: { key, suggestion } },
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2) apps 배열 형태 검증
|
|
47
|
+
if (globalConfig.apps !== undefined) {
|
|
48
|
+
if (
|
|
49
|
+
!Array.isArray(globalConfig.apps) ||
|
|
50
|
+
globalConfig.apps.some((n) => typeof n !== 'string')
|
|
51
|
+
) {
|
|
52
|
+
throw new MegaConfigError(
|
|
53
|
+
'config.invalid_apps',
|
|
54
|
+
"'apps' must be an array of app name strings (ADR-066).",
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3) wsHub.enabled + acceptedTokens 검증 (ADR-068)
|
|
60
|
+
if (globalConfig.wsHub?.enabled === true) {
|
|
61
|
+
const tokens = globalConfig.wsHub.acceptedTokens
|
|
62
|
+
if (!Array.isArray(tokens) || tokens.length === 0 || tokens.some((t) => !t)) {
|
|
63
|
+
throw new MegaConfigError(
|
|
64
|
+
'config.wsHub_empty_tokens',
|
|
65
|
+
'wsHub.acceptedTokens cannot be empty when wsHub.enabled is true.',
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4) Bridge↔Hub link 압축 검증 (ADR-078) — threshold 음수 / serverMaxWindowBits 범위.
|
|
71
|
+
const wsHubViolation = checkCompressionConfig(globalConfig.wsHub?.compression, 'wsHub.compression')
|
|
72
|
+
if (wsHubViolation) {
|
|
73
|
+
throw new MegaConfigError('config.invalid_compression', wsHubViolation, {
|
|
74
|
+
details: { scope: 'wsHub.compression' },
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5) jobs / schedules / workers 명시 등록 배열 검증 (M-2 + ADR-124 / 04-data-models §1.1).
|
|
79
|
+
// GLOBAL_ONLY_KEYS 에 키만 등록돼 있고 shape 검증이 없어, `jobs: SendEmailJob`(배열 잊음)이나
|
|
80
|
+
// 원소가 클래스 아닌 값이면 흡수 로직이 silent drop 했다. 부팅 시 fail-fast 로 드러낸다.
|
|
81
|
+
// 원소는 각 베이스 서브클래스(= 함수/클래스)여야 한다(register/buildWorkers 가 다시 정밀 검증).
|
|
82
|
+
validateClassArray(globalConfig.jobs, 'jobs', 'job', 'MegaJob')
|
|
83
|
+
validateClassArray(globalConfig.schedules, 'schedules', 'schedule', 'MegaSchedule')
|
|
84
|
+
validateClassArray(globalConfig.workers, 'workers', 'worker', 'MegaWorker')
|
|
85
|
+
|
|
86
|
+
// 6) server.sessionSecret 강도 검증 (M-1, 04-data-models §MegaServerConfig). 세션 쿠키 HMAC 키라
|
|
87
|
+
// 약한 시크릿은 세션 위조로 직결된다. 세션은 앱별 opt-in 이라 secret 누락(undefined)은 세션 사용
|
|
88
|
+
// 앱에서 registerSession 이 fail-fast 로 잡으므로(여긴 server.sessionSecret 가 "정의됐을 때만"
|
|
89
|
+
// 강도를 검증) — 정의됐다면 (a) 기본 placeholder 거부, (b) ≥32자 강제(부팅 fail-fast, ADR-062).
|
|
90
|
+
validateSessionSecret(globalConfig.server?.sessionSecret)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** sessionSecret 강도 검증의 최소 길이(바이트 수가 아닌 문자 길이 — base64url 32바이트 ≈ 43자). */
|
|
94
|
+
const SESSION_SECRET_MIN_LENGTH = 32
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `server.sessionSecret` 강도 검증 (M-1). 미정의(undefined)면 통과 — 세션 미사용 앱을 위한 선택성 유지.
|
|
98
|
+
* 정의됐다면: (1) `change-me*` placeholder 거부, (2) 길이 < 32자 거부. 둘 다 부팅 fail-fast(ADR-062).
|
|
99
|
+
*
|
|
100
|
+
* @param {unknown} secret - `globalConfig.server.sessionSecret` 값.
|
|
101
|
+
* @throws {MegaConfigError} `server.sessionSecret_default_unsafe` 또는 `server.sessionSecret_too_short`.
|
|
102
|
+
*/
|
|
103
|
+
function validateSessionSecret(secret) {
|
|
104
|
+
if (secret === undefined) return // 세션 미사용 앱 — 선택 키(세션 사용 시 registerSession 이 fail-fast).
|
|
105
|
+
// placeholder 거부를 길이보다 먼저 — '.env' 기본값 'change-me-in-production'(23자)이 "너무 짧음"
|
|
106
|
+
// 보다 "기본값 교체하라"는 메시지로 떠야 운영자가 원인을 즉시 안다.
|
|
107
|
+
if (typeof secret === 'string' && /^change-me/i.test(secret)) {
|
|
108
|
+
throw new MegaConfigError(
|
|
109
|
+
'server.sessionSecret_default_unsafe',
|
|
110
|
+
"server.sessionSecret is still the insecure default ('change-me…'). Set SESSION_SECRET to a random value (≥32 chars).",
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
if (typeof secret !== 'string' || secret.length < SESSION_SECRET_MIN_LENGTH) {
|
|
114
|
+
throw new MegaConfigError(
|
|
115
|
+
'server.sessionSecret_too_short',
|
|
116
|
+
`server.sessionSecret must be at least ${SESSION_SECRET_MIN_LENGTH} chars.`,
|
|
117
|
+
{ details: { length: typeof secret === 'string' ? secret.length : 0 } },
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `jobs`/`schedules`/`workers` 같은 "클래스(함수) 배열" config 키를 검증한다 (M-2/ADR-124). 미정의면
|
|
124
|
+
* 통과(선택 키). 정의됐으면 (1) 배열이어야 하고 (2) 모든 원소가 함수(클래스)여야 한다 — 위반 시 부팅 fail-fast.
|
|
125
|
+
*
|
|
126
|
+
* @param {unknown} value - globalConfig[key] 값.
|
|
127
|
+
* @param {'jobs' | 'schedules' | 'workers'} key - 검증 대상 키(에러 코드·메시지에 사용).
|
|
128
|
+
* @param {'job' | 'schedule' | 'worker'} singular - 원소 에러 코드용 단수형.
|
|
129
|
+
* @param {string} baseName - 사용자 메시지용 베이스 클래스명(예: 'MegaJob').
|
|
130
|
+
* @throws {MegaConfigError} 비배열(`global.invalid_<key>`) 또는 원소 비함수(`global.invalid_<singular>_class`).
|
|
131
|
+
*/
|
|
132
|
+
function validateClassArray(value, key, singular, baseName) {
|
|
133
|
+
if (value === undefined) return // 선택 키 — 미정의는 정상.
|
|
134
|
+
if (!Array.isArray(value)) {
|
|
135
|
+
throw new MegaConfigError(
|
|
136
|
+
`global.invalid_${key}`,
|
|
137
|
+
`'${key}' must be an array of ${baseName} subclasses (did you forget the array brackets?).`,
|
|
138
|
+
{ details: { key, type: typeof value } },
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < value.length; i++) {
|
|
142
|
+
if (typeof value[i] !== 'function') {
|
|
143
|
+
throw new MegaConfigError(
|
|
144
|
+
`global.invalid_${singular}_class`,
|
|
145
|
+
`'${key}[${i}]' must be a ${baseName} subclass (a class), got ${typeof value[i]}.`,
|
|
146
|
+
{ details: { key, index: i, type: typeof value[i] } },
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 앱 config 검증.
|
|
154
|
+
* @param {Record<string, any>} appConfig - apps/<name>/app.config.js export 객체
|
|
155
|
+
* @param {string} expectedFolderName - 폴더명 (apps/<name>)
|
|
156
|
+
* @param {Record<string, any>} globalConfig - 메인 (Shared-Reference 키 cross-check 용)
|
|
157
|
+
* @throws {MegaConfigError}
|
|
158
|
+
*/
|
|
159
|
+
export function validateAppConfig(appConfig, expectedFolderName, globalConfig) {
|
|
160
|
+
if (!appConfig || typeof appConfig !== 'object') {
|
|
161
|
+
throw new MegaConfigError(
|
|
162
|
+
'config.invalid',
|
|
163
|
+
`apps/${expectedFolderName}/app.config.js must export an object`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 1) 알 수 없는 키 + 잘못된 스코프
|
|
168
|
+
for (const key of Object.keys(appConfig)) {
|
|
169
|
+
if (APP_ONLY_KEYS.includes(key)) continue
|
|
170
|
+
if (GLOBAL_ONLY_KEYS.includes(key)) {
|
|
171
|
+
throw new MegaConfigError(
|
|
172
|
+
'config.wrong_scope',
|
|
173
|
+
`App config cannot define '${key}'. Move to mega.config.js (ADR-061).`,
|
|
174
|
+
{ details: { key, scope: 'global-only', app: expectedFolderName } },
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
const suggestion = suggestKey(key, [...APP_ONLY_KEYS, ...GLOBAL_ONLY_KEYS])
|
|
178
|
+
throw new MegaConfigError(
|
|
179
|
+
'config.unknown_key',
|
|
180
|
+
`Unknown key '${key}' in app config '${expectedFolderName}'.${suggestion ? ` Did you mean '${suggestion}'?` : ''}`,
|
|
181
|
+
{ details: { key, suggestion, app: expectedFolderName } },
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2) name 폴더 일치 검증 (ADR-067)
|
|
186
|
+
if (typeof appConfig.name !== 'string' || appConfig.name.length === 0) {
|
|
187
|
+
throw new MegaConfigError(
|
|
188
|
+
'config.missing_name',
|
|
189
|
+
`App config '${expectedFolderName}' must have a 'name' field.`,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
if (appConfig.name !== expectedFolderName) {
|
|
193
|
+
throw new MegaConfigError(
|
|
194
|
+
'config.name_mismatch',
|
|
195
|
+
`App config name '${appConfig.name}' does not match folder name '${expectedFolderName}'.`,
|
|
196
|
+
{ details: { configName: appConfig.name, folderName: expectedFolderName } },
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 3) Shared-Reference 키 검증 — 참조하는 키가 globalConfig.services 에 존재해야 함
|
|
201
|
+
for (const refKey of SHARED_REFERENCE_KEYS) {
|
|
202
|
+
if (!appConfig[refKey]) continue
|
|
203
|
+
const services = globalConfig?.services?.[refKey]
|
|
204
|
+
for (const [appAlias, globalKey] of Object.entries(appConfig[refKey])) {
|
|
205
|
+
if (typeof globalKey !== 'string') {
|
|
206
|
+
throw new MegaConfigError(
|
|
207
|
+
'config.invalid_reference',
|
|
208
|
+
`App '${appConfig.name}' ${refKey}.${appAlias} must be a string key referring to services.${refKey} in mega.config.js.`,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
if (!services || !services[globalKey]) {
|
|
212
|
+
throw new MegaConfigError(
|
|
213
|
+
'config.unknown_reference',
|
|
214
|
+
`App '${appConfig.name}' references ${refKey}.${globalKey} but '${globalKey}' is not defined in services.${refKey} in mega.config.js.`,
|
|
215
|
+
{ details: { app: appConfig.name, refKey, alias: appAlias, globalKey } },
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 4) 브라우저↔Bridge WS 압축 검증 (ADR-078) — threshold 음수 / serverMaxWindowBits 범위.
|
|
222
|
+
const wsViolation = checkCompressionConfig(appConfig.websocket?.compression, 'websocket.compression')
|
|
223
|
+
if (wsViolation) {
|
|
224
|
+
throw new MegaConfigError('config.invalid_compression', wsViolation, {
|
|
225
|
+
details: { app: appConfig.name, scope: 'websocket.compression' },
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 5) 브라우저↔Bridge WS 프레임 최대 크기 검증 (L-3, ADR-099) — 양의 정수만 허용.
|
|
230
|
+
// Hub(wsHub.maxPayloadBytes)와 대칭. 위반 시 부팅 fail-fast (ADR-062).
|
|
231
|
+
const maxPayload = appConfig.websocket?.maxPayloadBytes
|
|
232
|
+
if (maxPayload !== undefined && (!Number.isInteger(maxPayload) || maxPayload <= 0)) {
|
|
233
|
+
throw new MegaConfigError(
|
|
234
|
+
'config.invalid_max_payload',
|
|
235
|
+
`websocket.maxPayloadBytes must be a positive integer. Got ${maxPayload}`,
|
|
236
|
+
{ details: { app: appConfig.name, scope: 'websocket.maxPayloadBytes' } },
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 6) 정적 자산 prefix ↔ /metrics 경로 충돌 (ADR-071/072) — 같은 경로면 정적 라우트가 메트릭을 가려 부팅 throw.
|
|
241
|
+
// metricsPath 디폴트 '/metrics'. exposeMetrics OFF 면 메트릭 라우트가 없어 충돌 불가(검사 skip).
|
|
242
|
+
if (appConfig.staticAssets?.enabled === true && globalConfig?.health?.exposeMetrics === true) {
|
|
243
|
+
const metricsPath = globalConfig.health?.metricsPath ?? '/metrics'
|
|
244
|
+
const prefix = typeof appConfig.staticAssets.prefix === 'string' && appConfig.staticAssets.prefix.length > 0
|
|
245
|
+
? appConfig.staticAssets.prefix
|
|
246
|
+
: '/'
|
|
247
|
+
if (prefix === metricsPath) {
|
|
248
|
+
throw new MegaConfigError(
|
|
249
|
+
'config.static_metrics_conflict',
|
|
250
|
+
`App '${appConfig.name}' staticAssets.prefix '${prefix}' collides with health.metricsPath. Choose a different prefix.`,
|
|
251
|
+
{ details: { app: appConfig.name, prefix, metricsPath } },
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 모든 앱 cross-validate: 호스트 충돌 감지 (ADR-067).
|
|
259
|
+
* @param {Array<{ name: string, config: Record<string, any> }>} apps
|
|
260
|
+
* @throws {MegaConfigError}
|
|
261
|
+
*/
|
|
262
|
+
export function validateHostCollisions(apps) {
|
|
263
|
+
/** @type {Map<string, string>} host → 첫 점유 앱 이름 */
|
|
264
|
+
const hostMap = new Map()
|
|
265
|
+
for (const { name, config } of apps) {
|
|
266
|
+
if (!Array.isArray(config.hosts)) continue
|
|
267
|
+
for (const host of config.hosts) {
|
|
268
|
+
if (hostMap.has(host) && hostMap.get(host) !== name) {
|
|
269
|
+
throw new MegaConfigError(
|
|
270
|
+
'config.host_collision',
|
|
271
|
+
`Host '${host}' is claimed by both '${hostMap.get(host)}' and '${name}'.`,
|
|
272
|
+
{ details: { host, apps: [hostMap.get(host), name] } },
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
hostMap.set(host, name)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 가까운 키 추천 (Levenshtein 거리 기반).
|
|
282
|
+
* @param {string} input
|
|
283
|
+
* @param {readonly string[]} known
|
|
284
|
+
* @returns {string | undefined}
|
|
285
|
+
*/
|
|
286
|
+
export function suggestKey(input, known) {
|
|
287
|
+
let best = undefined
|
|
288
|
+
let bestDist = Infinity
|
|
289
|
+
const maxDist = Math.max(2, Math.floor(input.length / 3))
|
|
290
|
+
for (const candidate of known) {
|
|
291
|
+
const dist = levenshtein(input, candidate)
|
|
292
|
+
if (dist < bestDist && dist <= maxDist) {
|
|
293
|
+
best = candidate
|
|
294
|
+
bestDist = dist
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return best
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 단순 Levenshtein 거리 — 작은 문자열용.
|
|
302
|
+
* @param {string} a
|
|
303
|
+
* @param {string} b
|
|
304
|
+
* @returns {number}
|
|
305
|
+
*/
|
|
306
|
+
function levenshtein(a, b) {
|
|
307
|
+
if (a === b) return 0
|
|
308
|
+
const m = a.length
|
|
309
|
+
const n = b.length
|
|
310
|
+
if (m === 0) return n
|
|
311
|
+
if (n === 0) return m
|
|
312
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))
|
|
313
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i
|
|
314
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j
|
|
315
|
+
for (let i = 1; i <= m; i++) {
|
|
316
|
+
for (let j = 1; j <= n; j++) {
|
|
317
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
318
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return dp[m][n]
|
|
322
|
+
}
|