mega-framework 0.1.7 → 0.1.9
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/README.md +9 -0
- package/package.json +3 -3
- package/sample/crud/.env +9 -0
- package/sample/crud/.env.example +9 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +28 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/routes/upload.js +20 -1
- package/sample/crud/apps/main/services/guide-service.js +4 -3
- package/sample/crud/apps/main/services/upload-demo-service.js +6 -5
- package/sample/crud/apps/main/views/upload/index.ejs +4 -1
- package/sample/crud/docs/guide/01-cli.md +587 -0
- package/sample/crud/docs/guide/02-router-controller.md +497 -0
- package/sample/crud/docs/guide/03-service-model-db.md +929 -0
- package/sample/crud/docs/guide/04-websocket-asp.md +632 -0
- package/sample/crud/docs/guide/05-scheduler-job-worker.md +400 -0
- package/sample/crud/docs/guide/06-config-auth-session-security.md +495 -0
- package/sample/crud/docs/guide/07-view-i18n-static-multipart.md +462 -0
- package/sample/crud/docs/guide/08-observability.md +373 -0
- package/sample/crud/mega.config.js +7 -0
- package/sample/crud/package.json +2 -2
- package/sample/crud/scripts/start-ws-hub.sh +18 -4
- package/sample/crud/var/uploads/(/355/212/271/352/270/260/354/236/220) 2026 /353/251/264/354/240/221/354/225/210/353/202/264-mqbnwq5v-d2125aa8.txt" +1 -0
- package/sample/crud/var/uploads/(/355/212/271/352/270/260/354/236/220) 2026 /353/251/264/354/240/221/354/225/210/353/202/264-mqbo0nbf-842b6135.txt" +1 -0
- package/sample/crud/var/uploads/00_b-mqbnh7lc-c9790ab8.png +0 -0
- package/sample/crud/var/uploads/2025-07-22 13 35 56-mqbngvvk-e545e90e.png +0 -0
- package/sample/crud/var/uploads/big-file-mqbo1h9e-2957eaf5.png +0 -0
- package/sample/crud/var/uploads//341/204/200/341/205/247/341/206/274/341/204/200/341/205/265/341/204/211/341/205/265/341/206/257/341/204/214/341/205/245/341/206/250/341/204/214/341/205/263/341/206/274/341/204/206/341/205/247/341/206/274/341/204/211/341/205/245-mqbo5yxh-5288d8ef.pdf +0 -0
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/adapters/adapter-options.js +14 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +7 -4
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +4 -1
- package/src/adapters/mongo-adapter.js +21 -7
- package/src/adapters/postgres-adapter.js +8 -4
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +6 -2
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/scaffold.js +38 -2
- package/src/cli/generators/index.js +58 -1
- package/src/cli/index.js +88 -59
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +3 -1
- package/src/core/ctx-builder.js +59 -1
- package/src/core/envelope.js +9 -2
- package/src/core/hub-link.js +24 -14
- package/src/core/index.js +1 -1
- package/src/core/mega-app.js +55 -45
- package/src/core/pipeline.js +8 -6
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +3 -3
- package/src/core/session-store.js +14 -1
- package/src/core/ws-presence.js +17 -5
- package/src/core/ws-roster.js +49 -10
- package/src/core/ws-upgrade.js +105 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +10 -0
- package/src/lib/mega-job-queue.js +53 -13
- package/src/lib/mega-job.js +8 -1
- package/src/lib/mega-metrics.js +28 -1
- package/src/lib/mega-plugin.js +2 -2
- package/src/lib/mega-worker.js +28 -5
- package/src/lib/ws-hub.js +90 -9
- package/templates/adr/code.tpl +23 -0
- package/types/adapters/adapter-options.d.ts +2 -0
- package/types/adapters/file-adapter.d.ts +12 -1
- package/types/adapters/file-session-adapter.d.ts +4 -2
- package/types/adapters/maria-adapter.d.ts +5 -3
- package/types/adapters/mega-cache-adapter.d.ts +27 -1
- package/types/adapters/mega-db-adapter.d.ts +4 -1
- package/types/adapters/mongo-adapter.d.ts +13 -2
- package/types/adapters/postgres-adapter.d.ts +4 -2
- package/types/adapters/redis-adapter.d.ts +8 -0
- package/types/adapters/sqlite-adapter.d.ts +8 -2
- package/types/cli/generators/index.d.ts +11 -1
- package/types/cli/index.d.ts +12 -27
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ctx-builder.d.ts +23 -0
- package/types/core/hub-link.d.ts +3 -1
- package/types/core/index.d.ts +1 -1
- package/types/core/mega-app.d.ts +1 -1
- package/types/core/pipeline.d.ts +2 -1
- package/types/core/security.d.ts +3 -3
- package/types/core/session-store.d.ts +7 -0
- package/types/core/ws-roster.d.ts +13 -1
- package/types/core/ws-upgrade.d.ts +29 -0
- package/types/lib/mega-circuit-breaker.d.ts +4 -2
- package/types/lib/mega-health.d.ts +7 -0
- package/types/lib/mega-job-queue.d.ts +16 -4
- package/types/lib/mega-job.d.ts +8 -1
- package/types/lib/mega-plugin.d.ts +1 -1
- package/types/lib/mega-worker.d.ts +3 -1
- package/types/lib/ws-hub.d.ts +27 -2
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# Config + .env + Auth + Session + Security
|
|
2
|
+
|
|
3
|
+
설정 로딩부터 인증·세션·보안 플러그인까지, "앱을 안전하게 띄우는 데 필요한 모든 길목"을 한 곳에 모은 가이드다. 예시는 전부 `sample/crud` 의 실제 코드에서 가져왔다.
|
|
4
|
+
|
|
5
|
+
> 관련 ADR: 049, 061, 062, 066, 067, 102, 109, 127, 129, 130, 143, 146, 154, 155, 156, 159, 164, 167, 168
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. mega.config.js 구조
|
|
10
|
+
|
|
11
|
+
설정은 **두 파일**로 나뉜다. 어느 파일에 무엇을 쓰는지가 강제(부팅 throw)되므로 스코프부터 이해해야 한다.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
mega.config.js ← Global 스코프 (프로젝트 루트, 전역 자원·서버·관측성)
|
|
15
|
+
apps/<name>/app.config.js ← App 스코프 (앱별 보안·세션·뷰 + Shared-Reference)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 1.1 스코프 3종 (ADR-061)
|
|
19
|
+
|
|
20
|
+
`src/core/scope-registry.js` 가 키를 세 묶음으로 강제한다.
|
|
21
|
+
|
|
22
|
+
| 스코프 | 허용 파일 | 키 |
|
|
23
|
+
|--------|-----------|----|
|
|
24
|
+
| **Global-only** | `mega.config.js` | `services`, `server`, `wsHub`, `logger`, `apps`, `asp`, `health`, `tracing`, `plugins`, `jobs`, `schedules`, `workers` |
|
|
25
|
+
| **App-only** | `app.config.js` | `name`, `hosts`, `cors`, `helmet`, `rateLimit`, `csrf`, `session`, `bodyLimits`, `upload`, `i18n`, `views`, `asp`, `bridgeHub`, `openapi`, `staticAssets`, `websocket` |
|
|
26
|
+
| **Shared-Reference** | `app.config.js` | `databases`, `caches`, `buses` (전역 `services.*` 의 키를 **이름으로 참조**만) |
|
|
27
|
+
|
|
28
|
+
규칙을 어기면 부팅이 즉시 멈춘다(`src/core/config-validator.js`):
|
|
29
|
+
|
|
30
|
+
- 글로벌에 App-only 키 → `config.wrong_scope` ("Move to apps/<name>/app.config.js")
|
|
31
|
+
- 모르는 키 → `config.unknown_key` + **오타 추천**(Levenshtein, `Did you mean 'session'?`)
|
|
32
|
+
- 앱 `name` ↔ 폴더명 불일치 → `config.name_mismatch`
|
|
33
|
+
- 앱이 참조한 `databases.db` 가 전역 `services.databases` 에 없음 → `config.unknown_reference`
|
|
34
|
+
|
|
35
|
+
> 왜 강제하나: "설정을 엉뚱한 파일에 적어 조용히 무시되는" 사고를 부팅 시점에 드러내기 위해서다(fail-fast, ADR-062).
|
|
36
|
+
|
|
37
|
+
### 1.2 mega.config.js 예시 (sample/crud)
|
|
38
|
+
|
|
39
|
+
전역 파일에는 **공유 자원만** 둔다. 비밀은 전부 `process.env`(`.env`)에서 읽는다.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// sample/crud/mega.config.js
|
|
43
|
+
export default {
|
|
44
|
+
apps: ['main'], // 활성 앱 whitelist (ADR-066). 여기 없는 폴더는 로드 안 됨.
|
|
45
|
+
|
|
46
|
+
server: {
|
|
47
|
+
port: Number(process.env.PORT ?? 3000),
|
|
48
|
+
sessionSecret: process.env.SESSION_SECRET, // 세션 쿠키 HMAC 서명 키 (ADR-129)
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
asp: { masterSecret: process.env.ASP_MASTER_SECRET }, // 전역 시크릿
|
|
52
|
+
|
|
53
|
+
services: {
|
|
54
|
+
databases: {
|
|
55
|
+
primary: { driver: 'postgres', url: process.env.DATABASE_URL },
|
|
56
|
+
mongo: { driver: 'mongodb', url: process.env.MONGO_URL },
|
|
57
|
+
},
|
|
58
|
+
caches: {
|
|
59
|
+
rate: { driver: 'redis', url: process.env.REDIS_RATE_URL }, // brute-force 백엔드
|
|
60
|
+
demo: { driver: 'redis', url: process.env.REDIS_DEMO_URL },
|
|
61
|
+
lock: { driver: 'redis', url: process.env.REDIS_LOCK_URL },
|
|
62
|
+
},
|
|
63
|
+
buses: { jobs: { driver: 'nats', url: process.env.NATS_JOBS_URL } },
|
|
64
|
+
locks: { main: { driver: 'redlock', redis: 'lock' } },
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
logger: {
|
|
68
|
+
level: 'debug',
|
|
69
|
+
sinks: [{ type: 'console', pretty: true }],
|
|
70
|
+
redact: ['*.password', '*.token', '*.secret', '*.authorization'], // 민감필드 자동 마스킹
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 1.3 app.config.js 예시 (sample/crud)
|
|
76
|
+
|
|
77
|
+
앱 파일에는 **보안·세션·뷰** 같은 앱별 정책 + 전역 자원의 별명을 둔다.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// sample/crud/apps/main/app.config.js
|
|
81
|
+
export default {
|
|
82
|
+
name: 'main', // 폴더명과 일치해야 함 (ADR-067)
|
|
83
|
+
hosts: ['localhost', 'main.localhost'], // CSRF Origin allowlist 로도 쓰임 (ADR-051)
|
|
84
|
+
|
|
85
|
+
// Shared-Reference: alias → globalKey (services 의 키를 참조)
|
|
86
|
+
databases: { db: 'primary', mongo: 'mongo' },
|
|
87
|
+
caches: { rate: 'rate', demo: 'demo' },
|
|
88
|
+
buses: { jobs: 'jobs' },
|
|
89
|
+
|
|
90
|
+
session: { store: { driver: 'redis', url: process.env.REDIS_SESSION_URL }, ttlMs: 86_400_000, rolling: true },
|
|
91
|
+
rateLimit: { max: 600, timeWindow: '1 minute' }, // 기본 100/분 상향 (ADR-073)
|
|
92
|
+
helmet: { contentSecurityPolicy: { directives: { scriptSrc: ["'self'", "'wasm-unsafe-eval'"] } } },
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 1.4 per-instance 설정 — 공통 default 없음
|
|
97
|
+
|
|
98
|
+
DB 풀(pool) 같은 옵션은 **어댑터 인스턴스마다 개별 지정**한다. "모든 DB 에 공통으로 먹는 default 블록" 같은 건 없다. 이유: 인스턴스마다 부하·드라이버가 달라 공통 default 가 오히려 잘못된 값을 조용히 퍼뜨리기 때문이다. (→ §8 함정)
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 2. .env 사용 + env-mapper (ADR-109)
|
|
103
|
+
|
|
104
|
+
### 2.1 두 가지 읽기 경로
|
|
105
|
+
|
|
106
|
+
`.env` 값이 코드에 닿는 길은 두 가지다.
|
|
107
|
+
|
|
108
|
+
1. **직접 읽기** — config 파일에서 `process.env.DATABASE_URL` 처럼 그대로 꺼낸다(위 §1 예시). 가장 단순하고, 대부분의 sample 이 이 방식이다.
|
|
109
|
+
2. **env-mapper 자동 매핑** — `MEGA_<SERVICE>_*` 규칙을 따르는 환경변수를 어댑터 옵션 객체로 **자동 변환**한다. 12-factor 처럼 모든 옵션을 외부 주입하고 싶을 때 쓴다.
|
|
110
|
+
|
|
111
|
+
### 2.2 env-mapper 매핑 규칙 (`src/lib/env-mapper.js`)
|
|
112
|
+
|
|
113
|
+
`buildAdapterEnvConfig(service, env, { driver })` 가 `MEGA_<SERVICE>_` prefix 를 떼고 suffix 로 변환한다.
|
|
114
|
+
|
|
115
|
+
| 환경변수 | 결과 |
|
|
116
|
+
|----------|------|
|
|
117
|
+
| `MEGA_PG_URL=postgres://…` | `{ url: '…' }` |
|
|
118
|
+
| `MEGA_PG_HOST` / `PORT` / `USER` / `PASSWORD` | `{ host, port, user, password }` |
|
|
119
|
+
| `MEGA_PG_DB` 또는 `MEGA_PG_DATABASE` | `{ database }` |
|
|
120
|
+
| `MEGA_MONGO_DBNAME` | `{ dbName }` (Mongo) |
|
|
121
|
+
| `MEGA_PG_POOL_MAX=10` | `{ pool: { max: 10 } }` (항상 camelCase) |
|
|
122
|
+
| `MEGA_PG_OPTIONS_STATEMENT_TIMEOUT` | `{ options: { statement_timeout } }` (**driver별 표기**) |
|
|
123
|
+
|
|
124
|
+
**driver-aware OPTIONS_\*** — 드라이버마다 옵션 키 표기가 다르다. pg/sqlite 는 snake_case(`statement_timeout`), redis/nats/mongo/mariadb 는 camelCase(`keyPrefix`, `serverSelectionTimeoutMS`). env-mapper 는 `driver` 를 받아 표기를 맞춰준다. driver 를 모르면 snake 로 두고 **warn 1회**(추측 변환 금지).
|
|
125
|
+
|
|
126
|
+
**Redis `db` 특례** — `driver:'redis'` 일 때 `MEGA_REDIS_DB=1` 은 connection 이 아니라 논리 DB 번호(`{ db: 1 }`, 정수)다.
|
|
127
|
+
|
|
128
|
+
**타입 변환은 POOL_\*·OPTIONS_\* 만** — `'true'/'false'/'null'`→boolean/null, 정수/소수→Number. **연결 필드(url/host/user/password/database)는 항상 문자열**로 보존한다(비밀번호 `12345` 가 숫자로 깨지는 걸 막음). port·redis db 만 정수 강제.
|
|
129
|
+
|
|
130
|
+
### 2.3 .env.example 참조
|
|
131
|
+
|
|
132
|
+
리포에는 세 개의 `.env.example` 이 있다. 복사해서 `.env` 로 쓴다.
|
|
133
|
+
|
|
134
|
+
- **루트 `.env.example`** — framework 자체용(통합 테스트 인프라 `MEGA_*` + 관측성 + `SESSION_SECRET`).
|
|
135
|
+
- **`sample/crud/.env.example`** — crud 앱이 직접 읽는 키(아래).
|
|
136
|
+
- **`sample/simple/.env.example`** — 최소 구성.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# sample/crud/.env.example (발췌)
|
|
140
|
+
PORT=3000
|
|
141
|
+
DATABASE_URL=postgres://mega:change-me@localhost:5432/mega_test
|
|
142
|
+
SESSION_SECRET=change-me-to-a-long-random-secret # ≥32자 필수
|
|
143
|
+
|
|
144
|
+
# Redis — DB 인덱스로 용도 분리 (키 충돌 방지)
|
|
145
|
+
REDIS_SESSION_URL=redis://:change-me@localhost:6379/0 # 세션
|
|
146
|
+
REDIS_DEMO_URL=redis://:change-me@localhost:6379/1 # 데모 캐시
|
|
147
|
+
REDIS_RATE_URL=redis://:change-me@localhost:6379/2 # brute-force
|
|
148
|
+
REDIS_LOCK_URL=redis://:change-me@localhost:6379/3 # 분산 락
|
|
149
|
+
|
|
150
|
+
NATS_JOBS_URL=nats://localhost:4222
|
|
151
|
+
MONGO_URL=mongodb://mega:change-me@localhost:27017/mega_test?authSource=admin
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
> 한 Redis 인스턴스를 여러 용도가 공유할 때는 **논리 DB 번호(`/0`~`/3`)로 네임스페이스를 나눈다.** 세션·brute-force·데모 캐시가 같은 키 공간에서 섞이지 않게.
|
|
155
|
+
|
|
156
|
+
### 2.4 NODE_ENV=production 권장 (ADR-164)
|
|
157
|
+
|
|
158
|
+
`NODE_ENV` 는 여러 길목의 게이트다 — 템플릿 캐시, 정적자산, OpenAPI 노출, i18n `saveMissing`(dev 만), 메트릭 environment 라벨. 운영 배포는 **반드시 `NODE_ENV=production`** 으로 둔다. 미설정 시 일부 길목은 production 으로 간주하지만, 명시하는 게 안전하다.
|
|
159
|
+
|
|
160
|
+
> ⚠️ 로컬 통합 테스트 환경변수 이름 함정: 통합 테스트는 `MEGA_*_URL` 을 기대하는데 `.env` 는 `PG_URL` 같은 다른 이름을 쓴다. 로컬 실행 시 수동 매핑이 필요할 수 있다.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 3. Auth (ADR-143, sample ADR-155)
|
|
165
|
+
|
|
166
|
+
인증은 **세션 기반**이다. 비밀번호 검증 로직(`AuthService`)과 라우트 가드(`requireAuth`)는 분리돼 있다.
|
|
167
|
+
|
|
168
|
+
### 3.1 AuthService — register / authenticate
|
|
169
|
+
|
|
170
|
+
검증 로직은 서비스에 둔다(`sample/crud/apps/main/services/auth-service.js`). 파일명 `auth-service.js` → 자동 DI 이름 `auth` → `ctx.services.auth` 로 접근.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
export class AuthService extends MegaService {
|
|
174
|
+
static MIN_PASSWORD = 8 // OWASP 최소 권장
|
|
175
|
+
|
|
176
|
+
async register(input) {
|
|
177
|
+
const name = typeof input?.name === 'string' ? input.name.trim() : ''
|
|
178
|
+
const email = typeof input?.email === 'string' ? input.email.trim().toLowerCase() : ''
|
|
179
|
+
const password = typeof input?.password === 'string' ? input.password : ''
|
|
180
|
+
if (!name || !email) throw new MegaValidationError('auth.invalid', 'name and email are required', /* … */)
|
|
181
|
+
if (password.length < AuthService.MIN_PASSWORD) throw new MegaValidationError('auth.invalid', /* … */)
|
|
182
|
+
|
|
183
|
+
const passwordHash = await MegaHash.password.hash(password) // scrypt (ADR-130)
|
|
184
|
+
try {
|
|
185
|
+
return await User.register({ name, email, passwordHash })
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (err?.code === '23505') throw new MegaConflictError('user.email_taken', /* … */) // unique violation → 409
|
|
188
|
+
throw err
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async authenticate(input) {
|
|
193
|
+
const email = /* trim+lowercase */
|
|
194
|
+
const password = /* … */
|
|
195
|
+
if (!email || !password) return null
|
|
196
|
+
const row = await User.findByEmailWithHash(email)
|
|
197
|
+
if (row === null || typeof row.password_hash !== 'string') return null // 없는 이메일 / 비밀번호 미설정
|
|
198
|
+
const ok = await MegaHash.password.verify(password, row.password_hash)
|
|
199
|
+
if (!ok) return null
|
|
200
|
+
await User.touchLastLogin(row.id)
|
|
201
|
+
return { id: row.id, name: row.name }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> **user enumeration 방지**: `authenticate` 는 실패 이유(없는 이메일 / 비밀번호 불일치)를 구분하지 않고 **항상 `null`** 을 돌려준다. "이메일이 없다"와 "비밀번호가 틀렸다"를 구분해 주면 공격자가 계정 존재 여부를 알아낸다(OWASP Authentication Cheat Sheet).
|
|
207
|
+
|
|
208
|
+
### 3.2 scrypt 해싱 (ADR-130 / `src/lib/mega-hash.js`)
|
|
209
|
+
|
|
210
|
+
`MegaHash.password.{hash, verify}` 가 비밀번호를 scrypt 로 해시한다.
|
|
211
|
+
|
|
212
|
+
- **왜 scrypt** — argon2 는 네이티브 빌드 의존(node-gyp)이라 zero-dep 정책과 충돌. scrypt 는 Node 빌트인(`node:crypto`)이고 OWASP 가 argon2 다음으로 권장하는 memory-hard 함수다(ADR-130 이 ADR-050 supersede).
|
|
213
|
+
- **파라미터** — 디폴트 `N=2^15(32768), r=8, p=1`(≈32 MiB/해시). OWASP 권장 `N=2^17` 을 한 단계 낮췄다. 이유: 요청마다 128 MiB 를 할당하면 동시 로그인 폭주 시 서버가 자기-DoS(OOM)에 빠진다. bcrypt 실효 강도를 크게 웃돌면서 메모리도 안전한 균형점이다.
|
|
214
|
+
- **포맷(self-describing)** — `$scrypt$N=..,r=..,p=..$<salt>$<hash>`. 파라미터가 해시 문자열에 임베드돼, 나중에 N 을 올려도 기존 해시는 그대로 검증된다(점진 업그레이드).
|
|
215
|
+
- **fail-closed** — `verify(plain, hash)` 는 boolean 을 반환한다. 포맷이 깨졌거나 우리 해시가 아니면 **false**(불일치)다 — 잘못된 해시로 로그인을 통과시키지 않는다. 비교는 항상 `timingSafeEqual`(타이밍 공격 회피).
|
|
216
|
+
- **방어적 상한** — 손상된 해시가 거대 N(예 2^30)을 담고 있으면 OOM 위험. `N≤2^20, r≤32, p≤16` 초과는 검증 거부(false).
|
|
217
|
+
|
|
218
|
+
### 3.3 brute-force (ADR-049/130 / `src/lib/mega-brute-force.js`)
|
|
219
|
+
|
|
220
|
+
같은 대상이 정해진 횟수 이상 **실패**하면 일정 시간 잠근다(fixed window + lockout). 백엔드는 원자적 `INCR` 가 필요해 **Redis 필수**.
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
// sample/crud/apps/main/controllers/auth-controller.js — login
|
|
224
|
+
const subject = `${req.ip}:${email}` // ⭐ IP:email 조합 (단순 email 아님)
|
|
225
|
+
const bf = ctx.bruteForce // caches.rate 별명 → MegaBruteForce (lazy)
|
|
226
|
+
|
|
227
|
+
const status = await bf.check(subject) // 시도 전 조회 (부수효과 없음)
|
|
228
|
+
if (status.isLocked) return /* 423 Locked 폼 재렌더 */
|
|
229
|
+
|
|
230
|
+
const user = await ctx.services.auth.authenticate({ email, password })
|
|
231
|
+
if (!user) {
|
|
232
|
+
const after = await bf.fail(subject) // 실패 +1, 임계 도달 시 잠금
|
|
233
|
+
return /* after.isLocked ? 423 : 401 */
|
|
234
|
+
}
|
|
235
|
+
await bf.reset(subject) // 성공 → 카운터·잠금 즉시 제거
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
> ⭐ **subject 는 반드시 `IP:email` 조합으로.** email 단독으로 잠그면, 공격자가 피해자 email 로 일부러 반복 실패시켜 **정상 사용자를 잠가버리는** 계정 잠금 DoS(account-lockout DoS)가 가능하다(I-1). IP 를 섞으면 공격자는 자기 IP 만 잠그게 된다.
|
|
239
|
+
|
|
240
|
+
`ctx.bruteForce` 는 기본 정책(`maxAttempts:5, windowMs:15분, lockMs:15분`)에 `caches.rate` 백엔드를 쓰는 단축이다(`src/core/mega-app.js`). 도메인별로 다른 정책(예 password-reset)이 필요하면 직접 구성한다:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
new MegaBruteForce({ cache: ctx.cache('rate'), key: 'password-reset', maxAttempts: 3, lockMs: 30 * 60_000 })
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 3.4 라우트 가드 — requireAuth / requireRole / webRequireAuth
|
|
247
|
+
|
|
248
|
+
프레임워크는 `mega-framework/auth` 에서 가드를 export 한다(`src/auth/index.js`). 라우트 옵션 `{ before: [guard] }` 로 붙인다.
|
|
249
|
+
|
|
250
|
+
| 가드 | 출처 | 미인증 시 동작 | 용도 |
|
|
251
|
+
|------|------|----------------|------|
|
|
252
|
+
| `requireAuth` | `mega-framework/auth` | **401** throw (`auth.required`) | JSON REST API |
|
|
253
|
+
| `requireRole('admin')` | `mega-framework/auth` | 401(미로그인) / **403**(역할 부족) | 권한 제한 API |
|
|
254
|
+
| `webRequireAuth` | 샘플 `middleware/web-auth.js` | 로그인 페이지로 **리다이렉트(302)** | 브라우저 관리 UI |
|
|
255
|
+
|
|
256
|
+
> 프레임워크 기본 가드(`requireAuth`)는 JSON 401 을 던진다 — API 에 맞다. 브라우저로 보는 `/admin/**` 은 401 본문 대신 로그인 페이지로 보내는 게 자연스러우므로, 샘플이 **리다이렉트 가드(`webRequireAuth`)를 따로** 둔다. (브리핑이 말한 "apiRequireAuth" 라는 이름의 export 는 실제로 없다 — API 가드의 정본 이름은 `requireAuth` 다.)
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
// JSON API — sample/crud/apps/main/routes/users.js
|
|
260
|
+
import { requireAuth } from 'mega-framework/auth'
|
|
261
|
+
const guarded = { before: [requireAuth] }
|
|
262
|
+
router.http.get('/users', UserController.list, guarded)
|
|
263
|
+
|
|
264
|
+
// 관리 UI — sample/crud/apps/main/routes/web.js
|
|
265
|
+
import { webRequireAuth } from '../middleware/web-auth.js'
|
|
266
|
+
router.http.get('/admin/users', WebController.list, { before: [webRequireAuth] })
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
가드는 세션의 `userId` 로 신원을 확인하고 `req.user = { id, roles }` 를 심는다. silent fallback 없음(P4) — 인증 실패는 항상 표면화된다.
|
|
270
|
+
|
|
271
|
+
> 로그인/회원가입 라우트(`/auth/login`, `/register`)는 `/admin/**` 보호 영역 **밖**에 둔다. 안에 두면 비로그인 → 로그인 리다이렉트 루프가 생긴다(ADR-155).
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 4. Session (ADR-129 / `src/core/session.js`)
|
|
276
|
+
|
|
277
|
+
세션 미들웨어는 보안 플러그인과 같은 패턴으로 앱 생성 시점에 자동 등록된다. 쿠키 ↔ sid ↔ 세션 레코드를 배선한다.
|
|
278
|
+
|
|
279
|
+
### 4.1 store — redis(운영) / file(개발)
|
|
280
|
+
|
|
281
|
+
`createSessionStore(storeConfig)`(`src/core/session-store.js`)가 driver 로 어댑터를 만든다.
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
session: {
|
|
285
|
+
store: { driver: 'redis', url: process.env.REDIS_SESSION_URL, keyPrefix: 'mega:sess:' },
|
|
286
|
+
ttlMs: 86_400_000, // 24h
|
|
287
|
+
rolling: true,
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- **redis** (`MegaRedisSessionAdapter`) — **운영/클러스터 권장**. `SET key json PX ttlMs`(원자적 저장+만료), `touch`=`PEXPIRE`(rolling), `cleanup`=**no-op**(Redis TTL 자동 만료). 키는 `mega:sess:<sid>`(캐시 키 `mega:cache:` 와 분리).
|
|
292
|
+
- **file** (`MegaFileSessionAdapter`) — **dev/단일 인스턴스**. 단일 JSON envelope 파일(temp write 후 `rename` 으로 atomic), 파일명은 sid 의 SHA-256(경로 안전). `cleanup()` 이 만료 파일을 스캔 삭제. driver 한 줄만 바꾸면 redis 로 전환된다.
|
|
293
|
+
|
|
294
|
+
### 4.2 sid·쿠키·서명
|
|
295
|
+
|
|
296
|
+
- **sid = ULID** (26자, 48-bit timestamp + 80-bit randomness, `crypto.randomBytes` zero-dep). 시간순 정렬 + 추측 어려움.
|
|
297
|
+
- **쿠키 `mega.sid`** — 값은 `sid.base64url(hmac_sha256(secret, sid))` 형태로 **HMAC 서명**된다. 변조되면 무효(서명 비교는 `timingSafeEqual`). secret 은 `server.sessionSecret`.
|
|
298
|
+
- 쿠키 기본값: `httpOnly` ON / `secure: 'auto'`(https 면 자동 Secure) / `sameSite: lax` / 24h.
|
|
299
|
+
|
|
300
|
+
### 4.3 세션 객체 계약
|
|
301
|
+
|
|
302
|
+
`req.session` / `ctx.session` 은 데이터를 직접 읽고 쓰는 객체다.
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
req.session.userId = user.id // 데이터 쓰기 → 응답 시 자동 저장
|
|
306
|
+
const id = req.session.userId // 읽기
|
|
307
|
+
await req.session.save() // 즉시 영속 (await 가능)
|
|
308
|
+
await req.session.destroy() // 로그아웃 (스토어 삭제 + 쿠키 제거)
|
|
309
|
+
await req.session.regenerate() // 세션 고정 방지 (새 sid 재발급, 데이터 유지)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- **변경 감지 자동 저장** — 로드 시점 스냅샷과 응답 시점을 비교해 **변경분만** 저장한다(express-session 관례). `save()` 를 명시 호출하지 않아도 mutation 은 응답 때 영속된다.
|
|
313
|
+
- **rolling TTL** — 로드된 기존 세션은 매 요청 응답 시 `touch(sid, ttlMs)` 로 만료를 연장한다(`rolling: true`).
|
|
314
|
+
- **lazy 생성** — 쿠키 없는 첫 방문은 세션 객체만 만들고 `save()` 전까지 영속·쿠키 발급을 안 한다. 봇/헬스체크가 빈 세션을 양산하지 않게.
|
|
315
|
+
- **예약 키** — `id`·`isNew`·`save`·`destroy`·`regenerate`·`secret` 은 메서드/게터라 **세션 데이터 키로 쓰면 안 된다**.
|
|
316
|
+
|
|
317
|
+
### 4.4 login 후 regenerate() — 세션 고정 방지
|
|
318
|
+
|
|
319
|
+
로그인 직후 sid 를 새로 발급해야 한다. 안 그러면 공격자가 미리 심어둔 sid 로 피해자 세션을 탈취할 수 있다(session fixation).
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// sample/crud/apps/main/controllers/auth-controller.js
|
|
323
|
+
async function establishSession(req, user) {
|
|
324
|
+
req.session.userId = user.id
|
|
325
|
+
req.session.userName = user.name
|
|
326
|
+
await req.session.regenerate() // ⭐ 기존 sid 폐기 + 새 sid, 데이터 유지
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 4.5 WS upgrade 인증 — readSession (ADR-159)
|
|
331
|
+
|
|
332
|
+
HTTP `'upgrade'` 핸드셰이크는 Fastify onRequest 훅을 타지 않아 `req.session` 이 없다(raw `IncomingMessage`). 그래서 WS 라우트의 `before(req)` 인증은 `readSession` 으로 쿠키를 **직접 파싱**해 세션을 **읽기 전용**으로 복원한다(rolling touch·쿠키 발급·저장 없음).
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
// sample/crud/apps/main/middleware/ws-auth.js
|
|
336
|
+
import { readSession } from 'mega-framework'
|
|
337
|
+
|
|
338
|
+
export function makeWsRequireAuth(app) {
|
|
339
|
+
const secret = process.env.SESSION_SECRET
|
|
340
|
+
if (!secret) throw new Error('makeWsRequireAuth: SESSION_SECRET is required (set it in .env).')
|
|
341
|
+
return async function wsRequireAuth(req) {
|
|
342
|
+
const sess = await readSession(req, { store: app.sessionStore, secret })
|
|
343
|
+
const userId = sess?.data.userId
|
|
344
|
+
if (userId == null) return false // 비로그인/위조/만료 → upgrade 401 (fail-closed)
|
|
345
|
+
return { userId: String(userId), sessionId: sess.sid, userName: sess.data.userName ?? '' }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
반환 객체는 채널 onConnect 의 `ctx.auth` 가 된다. 비로그인/서명 위조/만료는 모두 `null`→`false`(fail-closed) — 신원이 없으면 거부한다.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 5. Security (ADR-127 / `src/core/security.js`)
|
|
355
|
+
|
|
356
|
+
`registerSecurityPlugins` 가 앱 생성 시점에 검증된 Fastify 플러그인 4종을 **app.config 기반으로 자동 등록**한다. 매 프로젝트에서 수동 등록하다 잊는 사고를 막는다. 각 키는 `undefined`=안전 디폴트 / `false`=비활성 / `object`=옵션으로 등록.
|
|
357
|
+
|
|
358
|
+
### 5.1 helmet — 보안 헤더 + CSP
|
|
359
|
+
|
|
360
|
+
`@fastify/helmet`. 디폴트 ON. CSP, HSTS, X-Frame-Options 등을 자동으로 박는다.
|
|
361
|
+
|
|
362
|
+
```js
|
|
363
|
+
// CSP override 예시 — WASM(MegaSocket)을 쓰는 페이지는 'wasm-unsafe-eval' 필요
|
|
364
|
+
helmet: { contentSecurityPolicy: { directives: { scriptSrc: ["'self'", "'wasm-unsafe-eval'"] } } }
|
|
365
|
+
```
|
|
366
|
+
> `useDefaults`(기본 true)라 지정한 지시문만 교체되고 나머지는 helmet 기본을 유지한다.
|
|
367
|
+
|
|
368
|
+
### 5.2 cors — origin allowlist
|
|
369
|
+
|
|
370
|
+
`@fastify/cors`. 디폴트 ON 이되 **`origin: false`(교차출처 거부)** — 안전 디폴트. 외부 origin 을 허용하려면 명시한다.
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
cors: { origin: ['https://app.example.com'], credentials: true }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 5.3 csrf — 폼 토큰 + Origin 검증 (ADR-051)
|
|
377
|
+
|
|
378
|
+
`@fastify/csrf-protection` + `@fastify/cookie`(cookie double-submit). 디폴트 ON. 안전 메서드(GET/HEAD/OPTIONS)는 통과. 그 외는 content-type 으로 갈린다:
|
|
379
|
+
|
|
380
|
+
- **폼**(`urlencoded` / `multipart`) → `_csrf` 토큰 검증. HTML 폼 auto-submit 이 CSRF 의 주 공격 벡터다.
|
|
381
|
+
- **그 외**(JSON·빈 본문) → 토큰 면제 + **Origin 검증**. Origin/Referer 가 앱 도메인(`hosts`)과 불일치하면 403. Origin 없는 비브라우저 API 클라는 통과(CSRF 는 브라우저 쿠키 공격).
|
|
382
|
+
|
|
383
|
+
폼 뷰에는 토큰을 심는다:
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
// 컨트롤러 — 폼 렌더 시
|
|
387
|
+
return ctx.render('auth/login', { /* … */ csrfToken: reply.generateCsrf() })
|
|
388
|
+
```
|
|
389
|
+
```html
|
|
390
|
+
<!-- 뷰 — 폼 안에 hidden 필드 -->
|
|
391
|
+
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### 5.4 rate-limit — IP 기반 요청 제한
|
|
395
|
+
|
|
396
|
+
`@fastify/rate-limit`. 디폴트 ON, **100 req / 1 minute**(ADR-048). 멀티 인스턴스는 `caches.rate`(Redis) 백엔드로 분산 카운팅, 미선언 시 in-memory 폴백(단일 dev).
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
rateLimit: { max: 600, timeWindow: '1 minute' } // 완전 교체 (deep merge 아님, ADR-073)
|
|
400
|
+
rateLimit: false // 비활성
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
> ⚠️ **등록 순서** — `registerSecurityPlugins` 는 `/health` 라우트 등록 **이전**에 호출돼야 한다. rate-limit 의 라우트별 면제(`config.skipRateLimit`)가 실효하려면 limiter 가 글로벌 onRequest hook 으로 먼저 붙어야 하기 때문이다. 프레임워크가 이 순서를 보장한다.
|
|
404
|
+
|
|
405
|
+
### 5.5 multipart 한도 (ADR-133)
|
|
406
|
+
|
|
407
|
+
업로드는 옵트인. MIME 화이트리스트 + 개수·크기 제한으로 게이트한다.
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
upload: { maxFileSize: 5 * 1024 * 1024, maxFiles: 3, allowedMimeTypes: ['image/*', 'application/pdf', 'text/plain'] }
|
|
411
|
+
```
|
|
412
|
+
> 옵트인 안 하면 multipart 요청은 415 다.
|
|
413
|
+
|
|
414
|
+
### 5.6 거부 사유 트레이싱 (ADR-126)
|
|
415
|
+
|
|
416
|
+
보안 거부 시 활성 HTTP span 에 `mega.security.reason`(`csrf.origin_mismatch` / `csrf.invalid_token` / `rate_limit.exceeded`)을 박는다. 트레이싱 OFF 면 0 비용 no-op.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 6. ASP (별도 가이드 참조)
|
|
421
|
+
|
|
422
|
+
ASP(Application-layer Secure Protocol)는 WS/HTTP 페이로드 자체를 암호화하는 별도 계층이다. `asp.masterSecret`(global) + 앱 옵트인(`asp.websocket.namespaces` 등)으로 합성된다. 자세한 내용은 **ASP 가이드(B4)** 를 참조한다. 본 가이드에서는 config 스코프만 정리한다:
|
|
423
|
+
|
|
424
|
+
- **Global** `asp.masterSecret` — 키 유도의 루트 시크릿(`.env` 의 `ASP_MASTER_SECRET`).
|
|
425
|
+
- **App** `asp.websocket.namespaces` — 암호화(E: 프레임)할 WS 경로 목록. 나머지는 평문(P:).
|
|
426
|
+
- **App** `asp.http.enabledPaths` — HTTP ASP 를 적용할 경로(있을 때만 등록).
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 7. CLI port/host 검증 (ADR-168 / `src/cli/index.js`)
|
|
431
|
+
|
|
432
|
+
`mega start --port N --host H` 의 값은 **fail-closed** 로 검증한다 — 잘못된 값을 조용히 강등하지 않는다.
|
|
433
|
+
|
|
434
|
+
### 7.1 --port
|
|
435
|
+
|
|
436
|
+
`resolvePort(setting)`:
|
|
437
|
+
|
|
438
|
+
- 유효 도메인 **0~65535 정수**. 미지정(`undefined`)은 config/기본값(3000)에 위임.
|
|
439
|
+
- **값 없는 베어 `--port`**(=`true`) / **빈 값 `--port=`**(=`''`) / 정수 아님 / 범위 밖 → `config.invalid_port` throw.
|
|
440
|
+
- **명시적 `--port 0`**(OS 랜덤 포트)은 정상 값으로 허용.
|
|
441
|
+
|
|
442
|
+
> 왜 fail-closed 인가: `Number(true)=1`(특권 포트), `Number('')=0`(랜덤 포트), `Number('abc')=NaN`(어댑터 connect 까지 부팅한 뒤 `ERR_SOCKET_BAD_PORT` 로 늦게 throw) — 조용히 강등하면 운영자가 의도와 다른 포트를 쓰거나 cryptic 에러를 늦게 만난다(P4/P7).
|
|
443
|
+
|
|
444
|
+
### 7.2 --host
|
|
445
|
+
|
|
446
|
+
`resolveHost(setting)`:
|
|
447
|
+
|
|
448
|
+
- 비어 있지 않은 문자열은 그대로. 미지정은 `undefined`(config/기본 위임).
|
|
449
|
+
- **값 없는 베어 `--host`** / **빈 값 `--host=`** → `config.invalid_host` throw.
|
|
450
|
+
|
|
451
|
+
> 빈 호스트를 `MegaServer` 로 흘리면 Node 가 전 인터페이스(`::`)에 silent bind 해 의도와 달라진다.
|
|
452
|
+
|
|
453
|
+
### 7.3 우선순위
|
|
454
|
+
|
|
455
|
+
포트/호스트/클러스터 모두 **CLI 플래그 > 환경변수 > config** 순이다. 예: `mega start --port 8080` > `PORT` > `mega.config.js` 의 `server.port`.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## 8. 함정 (꼭 읽을 것)
|
|
460
|
+
|
|
461
|
+
- **`SESSION_SECRET` 은 production 필수, ≥32자.** config-validator 가 `server.sessionSecret` 이 정의됐을 때 강도를 검증한다 — `change-me*` placeholder 거부(`sessionSecret_default_unsafe`), 32자 미만 거부(`sessionSecret_too_short`). 둘 다 부팅 fail-fast. (세션 미사용 앱이면 secret 누락은 통과 — 단 세션을 쓰면 `registerSession` 이 fail-fast.)
|
|
462
|
+
|
|
463
|
+
- **redis 세션 → graceful disconnect 필요.** redis 세션 어댑터의 `_disconnect` 는 `quit`(graceful)이다. 부팅/종료 시 connect/disconnect 라이프사이클을 베이스 상태머신이 관리하므로, 앱 종료 경로에서 어댑터를 닫는다.
|
|
464
|
+
|
|
465
|
+
- **brute-force subject 는 `IP:email` 이 정본.** email 단독 키는 account-lockout DoS 다(§3.3). 공격자가 피해자 email 로 반복 실패시켜 정상 사용자를 잠근다.
|
|
466
|
+
|
|
467
|
+
- **i18n `saveMissing` 은 dev 한정, 모든 available 언어에 기입.** `NODE_ENV==='development'` 에서만 누락 키를 **설정된 모든 언어(ko·en) 파일에 동시에** 기입한다(ADR-164). 기입 값은 `defaultValue`(없으면 키 이름) — **영어값을 다른 언어에 복사하지 않는다**(번역 미완 상태를 키 이름으로 드러냄). 운영(`production`)에선 자동 기입 OFF.
|
|
468
|
+
|
|
469
|
+
- **per-DB 옵션 — 공통 default 만들지 말 것.** DB 풀 등은 인스턴스별로 개별 지정한다(§1.4). "모든 DB 공통 default" 를 만들면 인스턴스마다 다른 부하·드라이버에 잘못된 값을 조용히 퍼뜨린다.
|
|
470
|
+
|
|
471
|
+
- **CSRF + JSON API.** JSON 요청은 CSRF 토큰이 면제되는 대신 **Origin 검증**을 받는다. 비브라우저 API 클라(Origin 헤더 없음)는 통과하지만, 브라우저에서 JSON 을 보내면 Origin 이 `hosts` 와 일치해야 한다.
|
|
472
|
+
|
|
473
|
+
- **rate-limit 옵션은 완전 교체.** `rateLimit: { max: 600 }` 로 주면 디폴트와 deep merge 가 아니라 **완전 교체**된다(ADR-073). `timeWindow` 도 직접 적어야 한다.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 관련 ADR 요약
|
|
478
|
+
|
|
479
|
+
| ADR | 내용 |
|
|
480
|
+
|-----|------|
|
|
481
|
+
| 049 | MegaBruteForce (반복 시도 잠금, Redis) |
|
|
482
|
+
| 061 / 062 | config 3-스코프 강제 + fail-fast 검증 |
|
|
483
|
+
| 066 / 067 | apps whitelist + name↔폴더 일치·호스트 충돌 |
|
|
484
|
+
| 102 / 109 | 전역 services 공유 + env-mapper 자동 매핑 |
|
|
485
|
+
| 127 | 보안 플러그인 4종 자동 등록 |
|
|
486
|
+
| 129 | 세션 미들웨어 (redis/file store) |
|
|
487
|
+
| 130 | scrypt 해싱 + brute-force 배선/정책 |
|
|
488
|
+
| 143 / 155 | 인증 가드 (requireAuth) / 샘플 auth+session |
|
|
489
|
+
| 156 | 라우트 before 미들웨어 |
|
|
490
|
+
| 159 | readSession (WS upgrade 세션 복원) |
|
|
491
|
+
| 164 | sample prod 기본 + i18n saveMissing |
|
|
492
|
+
| 167 | 콘솔 graceful (D3) |
|
|
493
|
+
| 168 | CLI resolvePort / resolveHost |
|
|
494
|
+
</content>
|
|
495
|
+
</invoke>
|