@gaonjs/config 0.19.0 → 0.22.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/README.md +1 -1
- package/dist/types.d.ts +37 -1
- package/dist/wire.js +71 -0
- package/package.json +11 -6
package/README.md
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectionConfig } from '@gaonjs/data';
|
|
2
|
-
import type { SecurityOptions, ClientIpConfig } from '@gaonjs/web';
|
|
2
|
+
import type { SecurityOptions, AppSecurityOptions, ClientIpConfig } from '@gaonjs/web';
|
|
3
3
|
import type { AuthOptions, JwtAuthOptions, SharedPropsFn } from '@gaonjs/web';
|
|
4
4
|
/**
|
|
5
5
|
* DB 커넥션 맵 — 키는 커넥션 이름(§4.5), 값은 어댑터 설정. `main` 은 관례
|
|
@@ -33,6 +33,12 @@ export interface MailConfig {
|
|
|
33
33
|
readonly user?: string;
|
|
34
34
|
readonly pass?: string;
|
|
35
35
|
readonly secure?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 결정 357: STARTTLS 를 **강제**한다(서버가 STARTTLS 를 광고하지 않으면 전송 실패).
|
|
38
|
+
* 기본(생략)은 기회적 STARTTLS — 운영 SMTP 에서 다운그레이드로 자격이 평문 노출될 수
|
|
39
|
+
* 있으니, 자격 있는 운영 전송은 `secure: true`(465) 또는 `requireTls: true` 를 켠다.
|
|
40
|
+
*/
|
|
41
|
+
readonly requireTls?: boolean;
|
|
36
42
|
/** from 헤더 기본값. */
|
|
37
43
|
readonly defaultFrom?: string;
|
|
38
44
|
}
|
|
@@ -98,6 +104,14 @@ export interface WebConfig {
|
|
|
98
104
|
* `{ proxy: 1 }`(신뢰 홉 수) · `{ header: 'cf-connecting-ip' }`(신뢰 헤더).
|
|
99
105
|
*/
|
|
100
106
|
readonly clientIp?: ClientIpConfig;
|
|
107
|
+
/**
|
|
108
|
+
* 결정 356: 업로드(multipart) 한도. 생략 시 파일당 10MB · 최대 10개.
|
|
109
|
+
* `false` 는 multipart 파싱 비활성. 한도 초과는 413 + 수리 안내로 응답된다.
|
|
110
|
+
*/
|
|
111
|
+
readonly uploads?: {
|
|
112
|
+
readonly maxFileSize?: number;
|
|
113
|
+
readonly maxFiles?: number;
|
|
114
|
+
} | false;
|
|
101
115
|
}
|
|
102
116
|
/** 루트 설정 — gaon.config.ts 의 defineConfig 인자. */
|
|
103
117
|
export interface GaonConfig {
|
|
@@ -140,6 +154,18 @@ export interface AppSessionConfig {
|
|
|
140
154
|
* 쓴 defineAppConfig 리터럴이 excess-property 컴파일 에러였다).
|
|
141
155
|
*/
|
|
142
156
|
readonly csrf?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* 쿠키 Secure 토글(결정 295). 생략 시 NODE_ENV==='production' 이면 켬. 운영인데
|
|
159
|
+
* 비-TLS(사내 내부망 http 등)로 서빙하면 Secure 쿠키가 안 실려 로그인이 조용히
|
|
160
|
+
* 실패한다 — 그 경우에만 `false` 로 명시한다(웹 레이어는 지원했는데 이 전달
|
|
161
|
+
* 통로가 없어 우회로가 없었다 · 결정 271 csrf 누락과 동형 표류).
|
|
162
|
+
*/
|
|
163
|
+
readonly secure?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* 쿠키 SameSite(결정 295). 생략 시 'lax'. `'none'` 은 스펙상 Secure 필수라
|
|
166
|
+
* secure 가 꺼져 있으면 부팅 에러(웹 레이어 가드 · 브라우저 조용한 쿠키 거부 방지).
|
|
167
|
+
*/
|
|
168
|
+
readonly sameSite?: 'lax' | 'strict' | 'none';
|
|
143
169
|
}
|
|
144
170
|
/**
|
|
145
171
|
* 앱별 설정 — apps/<app>/app.config.ts (v0.16 §3.3).
|
|
@@ -178,6 +204,7 @@ export interface AppConfig {
|
|
|
178
204
|
*/
|
|
179
205
|
readonly seal?: boolean | {
|
|
180
206
|
readonly except?: readonly string[];
|
|
207
|
+
readonly strictQuery?: boolean;
|
|
181
208
|
};
|
|
182
209
|
/**
|
|
183
210
|
* 결정 150: 앱 전역 공유 prop 제공자. 요청마다 실행돼 반환 객체가 이 앱의 모든 렌더
|
|
@@ -186,6 +213,15 @@ export interface AppConfig {
|
|
|
186
213
|
* sharedProps: (ctx) => ({ locale: ctx.session?.locale ?? 'en' })
|
|
187
214
|
*/
|
|
188
215
|
readonly sharedProps?: SharedPropsFn;
|
|
216
|
+
/**
|
|
217
|
+
* 결정 339: 이 앱만의 보안 override — CORS·rate limit. 생략한 필드는 전역
|
|
218
|
+
* (`gaon.config.ts` 의 `web.security`)을 상속한다. API 앱만 크로스 오리진을 열고
|
|
219
|
+
* web 앱의 same-origin 기본은 유지하는 용도:
|
|
220
|
+
* security: { cors: { origin: 'https://app.example.com' }, rateLimit: { max: 600 } }
|
|
221
|
+
* `false` 는 이 앱에서 그 방어를 끈다(명시적으로만 · 규칙 8). 보안 응답 헤더는
|
|
222
|
+
* 전역 전용(앱 override 없음).
|
|
223
|
+
*/
|
|
224
|
+
readonly security?: AppSecurityOptions;
|
|
189
225
|
}
|
|
190
226
|
/** 타입 안전 헬퍼 — `export default defineAppConfig({...})`. */
|
|
191
227
|
export declare function defineAppConfig(config: AppConfig): AppConfig;
|
package/dist/wire.js
CHANGED
|
@@ -177,11 +177,19 @@ export async function configureNonDbBatteries(config, cwd = process.cwd(), opts
|
|
|
177
177
|
// mail — SMTP 전송(개발·테스트는 MailPit sink, 운영은 실 SMTP).
|
|
178
178
|
if (config.mail) {
|
|
179
179
|
const m = config.mail;
|
|
180
|
+
// 결정 357: user/pass 반쪽 설정은 fail-loud — 이전엔 `user && pass` 게이팅으로 auth 가
|
|
181
|
+
// 무신호 드랍돼(익명 전송 시도) "설정했는데 인증이 안 걸리는" 조용한 오배선이었다.
|
|
182
|
+
if (!!m.user !== !!m.pass) {
|
|
183
|
+
throw new Error(`mail 설정에 user/pass 가 한쪽만 있습니다 (auth 는 둘 다 필요) — 반쪽 설정은 인증 없이 전송을 시도합니다.\n` +
|
|
184
|
+
` → gaon.config.ts 의 mail 블록에 ${m.user ? 'pass' : 'user'} 를 함께 설정하거나(.env 의 SMTP_USER/SMTP_PASS),\n` +
|
|
185
|
+
` → 인증 없는 SMTP(MailPit 등)라면 둘 다 제거하세요.`);
|
|
186
|
+
}
|
|
180
187
|
configureMailer({
|
|
181
188
|
transport: smtpTransport({
|
|
182
189
|
host: m.host,
|
|
183
190
|
port: m.port,
|
|
184
191
|
secure: m.secure,
|
|
192
|
+
requireTls: m.requireTls,
|
|
185
193
|
auth: m.user && m.pass ? { user: m.user, pass: m.pass } : undefined,
|
|
186
194
|
}),
|
|
187
195
|
defaultFrom: m.defaultFrom,
|
|
@@ -217,6 +225,24 @@ export async function configureNonDbBatteries(config, cwd = process.cwd(), opts
|
|
|
217
225
|
const dir = config.i18n.dir ?? 'locales';
|
|
218
226
|
const abs = dir.startsWith('/') ? dir : join(root, dir);
|
|
219
227
|
const resources = loadLocales(abs);
|
|
228
|
+
// 결정 353: i18n 을 켰는데 카탈로그가 비면 fail-loud — 이전엔 무경고로 부팅해
|
|
229
|
+
// 모든 t() 가 원문 키를 화면에 노출했다(배포 이미지 locales 누락·dir 오타 무신호).
|
|
230
|
+
if (Object.keys(resources).length === 0) {
|
|
231
|
+
throw new Error(`i18n 이 설정됐지만 로케일 카탈로그가 비어 있습니다: ${abs}\n` +
|
|
232
|
+
` → ${dir}/${config.i18n.fallbackLng}.json 을 만들거나(최소 fallbackLng 카탈로그),\n` +
|
|
233
|
+
` → gaon.config.ts 의 i18n.dir 이 실제 카탈로그 폴더를 가리키는지 확인하세요.\n` +
|
|
234
|
+
` (이 상태로 부팅하면 모든 t() 가 번역 대신 원문 키를 노출합니다 — 결정 353)`);
|
|
235
|
+
}
|
|
236
|
+
// 결정 353: fallbackLng·supportedLngs 가 지목한 로케일 파일이 통째로 없으면 fail-loud —
|
|
237
|
+
// 이전엔 협상이 그 로케일을 골라 주고 전 키가 fallback 언어로 폴백돼, 로케일 하나가
|
|
238
|
+
// 조용히 다른 언어로 나갔다(locale-parity 는 존재 파일만 비교해 못 잡는 사각).
|
|
239
|
+
const missing = [config.i18n.fallbackLng, ...(config.i18n.supportedLngs ?? [])].filter((lng, i, arr) => arr.indexOf(lng) === i && !resources[lng]);
|
|
240
|
+
if (missing.length > 0) {
|
|
241
|
+
throw new Error(`i18n 설정이 지목한 로케일의 카탈로그 파일이 없습니다: ${missing.join(', ')}\n` +
|
|
242
|
+
` → ${dir}/ 에 ${missing.map((l) => `${l}.json`).join(' · ')} 을 추가하거나,\n` +
|
|
243
|
+
` → gaon.config.ts 의 fallbackLng/supportedLngs 에서 해당 로케일을 제거하세요.\n` +
|
|
244
|
+
` (파일이 없는 로케일은 전 화면이 fallback 언어로 조용히 대체됩니다 — 결정 353)`);
|
|
245
|
+
}
|
|
220
246
|
await configureI18n({
|
|
221
247
|
resources,
|
|
222
248
|
fallbackLng: config.i18n.fallbackLng,
|
|
@@ -325,6 +351,11 @@ async function assembleApp(config, root, domain, redis, nats) {
|
|
|
325
351
|
if (typeof ac?.sharedProps === 'function') {
|
|
326
352
|
spec = { ...spec, sharedProps: ac.sharedProps };
|
|
327
353
|
}
|
|
354
|
+
// 결정 339: app.config 의 앱 스코프 보안 override(CORS·rate limit)를 전달한다 —
|
|
355
|
+
// 생략 필드는 전역(web.security) 상속. createApp 이 앱 스코프 단위로 등록한다.
|
|
356
|
+
if (ac?.security && typeof ac.security === 'object') {
|
|
357
|
+
spec = { ...spec, security: ac.security };
|
|
358
|
+
}
|
|
328
359
|
if (ac?.session?.secret) {
|
|
329
360
|
// 결정 263: 세션이 구성됐는데(session.secret) Redis 가 없으면 fail-loud 한다.
|
|
330
361
|
// 세션 저장소는 Redis 가 필수인데(redisSessionStore), 이전엔 `&& redis` 게이팅으로
|
|
@@ -346,6 +377,12 @@ async function assembleApp(config, root, domain, redis, nats) {
|
|
|
346
377
|
// app.config 의 CSRF 토글을 serve 로 전달한다(SessionOptions.csrf · 명시적으로만 끔).
|
|
347
378
|
// 이전엔 wireGaon 이 이 필드를 떨궈 documented 토글이 gaon serve 에서 무시됐다.
|
|
348
379
|
csrf: ac.session.csrf,
|
|
380
|
+
// 결정 295: 쿠키 secure·sameSite 도 전달한다 — 웹 레이어(AppSessionOptions)는
|
|
381
|
+
// 지원했는데 이 통로가 떨궈 운영 비-TLS(secure:false)·sameSite 조정의 우회로가
|
|
382
|
+
// 없었다(결정 271 csrf 누락과 동형). sameSite:'none'+secure 미충족은 웹 레이어가
|
|
383
|
+
// 부팅 에러로 막는다(assertSameSiteSecure).
|
|
384
|
+
secure: ac.session.secure,
|
|
385
|
+
sameSite: ac.session.sameSite,
|
|
349
386
|
},
|
|
350
387
|
};
|
|
351
388
|
}
|
|
@@ -368,6 +405,12 @@ async function assembleApp(config, root, domain, redis, nats) {
|
|
|
368
405
|
security: config.web?.security,
|
|
369
406
|
// 결정 131: 스토리지 오리진 CSP 자동 배선(설정 있으면 img-src·connect-src 에 부착).
|
|
370
407
|
storageOrigins: storageCspOrigins(config.storage),
|
|
408
|
+
// 결정 355: 로컬 디스크 공개 경로(`/storage/<key>`)를 프레임웍이 직접 서빙 —
|
|
409
|
+
// url() 이 만드는 좌표를 아무도 응답하지 않던 조용한 404 를 없앤다.
|
|
410
|
+
localStorage: localStorageServeTargets(config.storage, root),
|
|
411
|
+
// 결정 356: 업로드 한도 config 표면(web.uploads) — 이전엔 createApp 옵션만 있고
|
|
412
|
+
// wireGaon 이 전달하지 않아 정본 경로(gaon.config.ts)로 한도를 올릴 방법이 없었다.
|
|
413
|
+
uploads: config.web?.uploads,
|
|
371
414
|
// 클라이언트 IP 정책(결정 120 · 생략 시 'direct'). 프록시 뒤 배치면 선언.
|
|
372
415
|
clientIp: config.web?.clientIp,
|
|
373
416
|
realtime: nats ? { nats, hubAddr: config.hub?.addr, heartbeatMs: config.hub?.heartbeatMs } : undefined,
|
|
@@ -462,6 +505,34 @@ void redisSessionStore;
|
|
|
462
505
|
* 표시 오리진(img-src). 로컬 디스크는 same-origin 상대 URL 이라 CSP 와 무관(제외). 오리진은
|
|
463
506
|
* `new URL(v).origin`(scheme+host+port) 만 — 경로·쿼리는 배제한다.
|
|
464
507
|
*/
|
|
508
|
+
/**
|
|
509
|
+
* 결정 355: 상대 publicUrl(기본 '/storage')을 가진 로컬 디스크 → 루트 서빙 대상.
|
|
510
|
+
* 절대 URL(별도 서버/CDN)은 앱이 서빙하지 않는다. 같은 경로에 다른 root 가 겹치면
|
|
511
|
+
* fail-loud(어느 디스크가 응답할지 비결정 — 조용한 오서빙 방지).
|
|
512
|
+
*/
|
|
513
|
+
function localStorageServeTargets(storage, root) {
|
|
514
|
+
if (!storage)
|
|
515
|
+
return [];
|
|
516
|
+
const out = [];
|
|
517
|
+
for (const [name, d] of Object.entries(storage.disks)) {
|
|
518
|
+
if (d.driver !== 'local')
|
|
519
|
+
continue;
|
|
520
|
+
const urlPrefix = d.publicUrl ?? '/storage';
|
|
521
|
+
if (!urlPrefix.startsWith('/'))
|
|
522
|
+
continue;
|
|
523
|
+
const abs = d.root.startsWith('/') ? d.root : join(root, d.root);
|
|
524
|
+
const dup = out.find((o) => o.urlPrefix === urlPrefix);
|
|
525
|
+
if (dup) {
|
|
526
|
+
if (dup.root !== abs) {
|
|
527
|
+
throw new Error(`storage 로컬 디스크 '${name}' 의 publicUrl('${urlPrefix}')이 다른 로컬 디스크와 겹칩니다(root 는 다름).\n` +
|
|
528
|
+
` → 디스크마다 publicUrl 을 다르게 지정하세요(예: '/storage-${name}').`);
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
out.push({ urlPrefix, root: abs });
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
465
536
|
function storageCspOrigins(storage) {
|
|
466
537
|
if (!storage)
|
|
467
538
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://gaonjs.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://git.nyx-zone.com/gaon/framework.git",
|
|
11
|
+
"directory": "packages/config"
|
|
12
|
+
},
|
|
8
13
|
"engines": {
|
|
9
14
|
"node": ">=22"
|
|
10
15
|
},
|
|
@@ -25,14 +30,14 @@
|
|
|
25
30
|
],
|
|
26
31
|
"dependencies": {
|
|
27
32
|
"ioredis": "^5.4.1",
|
|
28
|
-
"@gaonjs/adapter-mongo": "0.
|
|
33
|
+
"@gaonjs/adapter-mongo": "0.3.0",
|
|
29
34
|
"@gaonjs/core": "0.2.4",
|
|
35
|
+
"@gaonjs/async": "0.17.0",
|
|
36
|
+
"@gaonjs/data": "0.24.0",
|
|
30
37
|
"@gaonjs/i18n": "0.2.4",
|
|
31
|
-
"@gaonjs/
|
|
32
|
-
"@gaonjs/async": "0.15.3",
|
|
38
|
+
"@gaonjs/web": "0.27.0",
|
|
33
39
|
"@gaonjs/storage": "0.1.5",
|
|
34
|
-
"@gaonjs/
|
|
35
|
-
"@gaonjs/web": "0.21.0"
|
|
40
|
+
"@gaonjs/mail": "0.4.0"
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
38
43
|
"fastify": "^5.0.0"
|