@gaonjs/cli 0.56.0 → 0.57.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/dist/commands/dev.js +22 -3
- package/dist/doctor/dotenv-node-env.d.ts +5 -0
- package/dist/doctor/dotenv-node-env.js +68 -0
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +6 -1
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +8 -3
- package/dist/index.js +3 -3
- package/dist/nodeEnv.d.ts +23 -0
- package/dist/nodeEnv.js +26 -0
- package/dist/serve.js +13 -1
- package/dist/templates/project/AGENTS.md.tpl +27 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/agents/seal.md.tpl +9 -4
- package/package.json +7 -7
package/dist/commands/dev.js
CHANGED
|
@@ -30,6 +30,7 @@ import { existsSync } from 'node:fs';
|
|
|
30
30
|
import { resolve } from 'node:path';
|
|
31
31
|
import { loadDotEnv, VERSION } from '@gaonjs/core';
|
|
32
32
|
import { registerTsResolve } from '../tsResolve.js';
|
|
33
|
+
import { defaultNodeEnv } from '../nodeEnv.js';
|
|
33
34
|
import { startDev, resolveDevLayout } from '../dev.js';
|
|
34
35
|
import { generateTablesDts, watchDir } from '@gaonjs/data';
|
|
35
36
|
import { generateRoutesDts } from '@gaonjs/web';
|
|
@@ -84,7 +85,11 @@ function spawnServe(args) {
|
|
|
84
85
|
// 셸에서 Ctrl+C 를 누르면 자식도 같이 SIGINT 를 받는데(같은 프로세스
|
|
85
86
|
// 그룹), serve 는 자체 handler 로 graceful 종료하므로 안전.
|
|
86
87
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
|
-
|
|
88
|
+
// 결정 430: 부모의 **현재** env 가 아니라 dev 진입 시점 스냅샷을 준다. 부모 안에서
|
|
89
|
+
// 도는 라이브러리(vite build 는 이 프로세스에서 실행된다 · dev/build.ts)가 나중에
|
|
90
|
+
// process.env 를 만져도 자식이 흔들리지 않는다 — 최초 자식은 멀쩡한데 재기동된
|
|
91
|
+
// 자식만 오염되던 split-brain 을 구조로 제거한다.
|
|
92
|
+
env: args.env,
|
|
88
93
|
});
|
|
89
94
|
child.stdout?.on('data', (b) => args.console.pipe('serve', b, 'info'));
|
|
90
95
|
child.stderr?.on('data', (b) => args.console.pipe('serve', b, 'error'));
|
|
@@ -104,7 +109,9 @@ function spawnProcess(args) {
|
|
|
104
109
|
const child = spawn(node, [...nodeArgs, ...cmdArgs], {
|
|
105
110
|
cwd: args.cwd,
|
|
106
111
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
107
|
-
|
|
112
|
+
// 결정 430: spawnServe 와 같은 스냅샷 규칙 — hub 는 재기동하지 않으므로, 부모 env 를
|
|
113
|
+
// 그때그때 읽으면 hub 와 재기동된 serve·work 의 NODE_ENV 가 갈렸다(split-brain).
|
|
114
|
+
env: { ...args.env, ...args.extraEnv },
|
|
108
115
|
});
|
|
109
116
|
child.stdout?.on('data', (b) => args.console.pipe(args.kind, b, 'info'));
|
|
110
117
|
child.stderr?.on('data', (b) => args.console.pipe(args.kind, b, 'error'));
|
|
@@ -164,6 +171,15 @@ export async function runDevCommand(opts = {}) {
|
|
|
164
171
|
const signals = opts.signals ?? process;
|
|
165
172
|
registerTsResolve();
|
|
166
173
|
loadDotEnv(cwd);
|
|
174
|
+
// 결정 430: 모드는 명령이 정한다 — `gaon dev` 는 development. **vite 기동보다 먼저**
|
|
175
|
+
// 잡아야 한다: 아래 startFrontendBuild 가 vite.build 를 이 프로세스 안에서 부르는데,
|
|
176
|
+
// vite 의 resolveConfig(build)가 NODE_ENV 미설정이면 process.env.NODE_ENV 에
|
|
177
|
+
// 'production' 을 꽂는다(vite 7 config.js `isNodeEnvSet` 분기 · 2026-08-06 실측).
|
|
178
|
+
// 그러면 파일 저장으로 재기동된 serve 자식이 production 을 상속해 스캐폴드 dev
|
|
179
|
+
// 플레이스홀더 secret 이 prod 세션 게이트(결정 255)에 걸려 즉사했다.
|
|
180
|
+
defaultNodeEnv('development');
|
|
181
|
+
// 자식(serve·work·hub)에 물려줄 env 스냅샷 — 여기서 고정한다(결정 430).
|
|
182
|
+
const childEnv = { ...process.env };
|
|
167
183
|
const consoleOut = createDevConsole({ json, timestamp: opts.timestamp });
|
|
168
184
|
const startedAt = Date.now();
|
|
169
185
|
consoleOut.log('dev', `gaon dev · 시작 (v${VERSION}) — ${cwd}`);
|
|
@@ -231,6 +247,7 @@ export async function runDevCommand(opts = {}) {
|
|
|
231
247
|
port: opts.port,
|
|
232
248
|
host: opts.host,
|
|
233
249
|
console: consoleOut,
|
|
250
|
+
env: childEnv,
|
|
234
251
|
onExit: (code, signal) => {
|
|
235
252
|
if (shuttingDown)
|
|
236
253
|
return;
|
|
@@ -257,11 +274,12 @@ export async function runDevCommand(opts = {}) {
|
|
|
257
274
|
cwd,
|
|
258
275
|
json,
|
|
259
276
|
console: consoleOut,
|
|
277
|
+
env: childEnv,
|
|
260
278
|
// dev 워커는 잡을 병렬 처리한다(기본 1 → DEV_WORKER_CONCURRENCY). 헤드오브라인
|
|
261
279
|
// 블로킹을 없애 개발 반복을 빠르게 하고, 분산 락(중첩 방지) 같은 동시성 속성이
|
|
262
280
|
// 단일 dev 워커에서도 관측되게 한다. 사용자가 GAON_WORKER_CONCURRENCY 를 이미
|
|
263
281
|
// 주면 그 값을 존중한다(override 하지 않음).
|
|
264
|
-
extraEnv:
|
|
282
|
+
extraEnv: childEnv.GAON_WORKER_CONCURRENCY == null || childEnv.GAON_WORKER_CONCURRENCY === ''
|
|
265
283
|
? { GAON_WORKER_CONCURRENCY: String(DEV_WORKER_CONCURRENCY) }
|
|
266
284
|
: undefined,
|
|
267
285
|
onExit: (code, signal) => {
|
|
@@ -292,6 +310,7 @@ export async function runDevCommand(opts = {}) {
|
|
|
292
310
|
cwd,
|
|
293
311
|
json,
|
|
294
312
|
console: consoleOut,
|
|
313
|
+
env: childEnv,
|
|
295
314
|
onExit: (code, signal) => {
|
|
296
315
|
if (shuttingDown)
|
|
297
316
|
return;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RuleReport } from './types.js';
|
|
2
|
+
/** 한 줄이 NODE_ENV 대입이면 그 값을 낸다. 주석·export 접두·따옴표를 흡수한다. */
|
|
3
|
+
export declare function nodeEnvAssignment(line: string): string | undefined;
|
|
4
|
+
/** 공유 `.env` 파일에 박힌 NODE_ENV 를 경고한다. */
|
|
5
|
+
export declare function checkDotenvNodeEnv(cwd: string): Promise<RuleReport>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · `.env` 의 NODE_ENV 검사 (결정 430)
|
|
2
|
+
//
|
|
3
|
+
// **모드는 명령이 정한다** — `gaon dev` = development · `gaon serve` = production.
|
|
4
|
+
// `.env`(와 `.env.local`·`.env.example`)는 dev·prod·테스트가 **공유**하는 파일이라, 여기에
|
|
5
|
+
// NODE_ENV 를 박으면 그 값이 모든 실행 모드로 새어나간다:
|
|
6
|
+
// · `NODE_ENV=production` → `gaon dev` 가 production 으로 떠 세션 쿠키 Secure(비-TLS 로컬에서
|
|
7
|
+
// 로그인 불가)·dev 플레이스홀더 secret 거부(결정 255)로 부팅이 죽고, `gaon db reset` 이 거부된다.
|
|
8
|
+
// · `NODE_ENV=development` → 운영 배포가 development 로 떠 prod 게이트(플레이스홀더 secret 거부·
|
|
9
|
+
// 쿠키 Secure·락 in-memory 폴백 차단)가 통째로 안 걸린다. **부팅은 green, 보안만 꺼진다.**
|
|
10
|
+
// 어느 쪽이든 무신호 파손이라 정적 검사로 짚는다. fail-closed 는 아니다(warn) — 기존 프로젝트를
|
|
11
|
+
// 부팅 불가로 만들지 않고, 정당한 예외(컨테이너 이미지 안에서 `.env` 를 모드별로 생성하는 파이프라인)를
|
|
12
|
+
// 남긴다. 모드별 오버레이(`.env.development`·`.env.production`)는 이미 모드로 갈리므로 검사 대상이 아니다.
|
|
13
|
+
//
|
|
14
|
+
// 판정은 라인 텍스트 기반 — `.env` 는 셸 문법이 아니라 KEY=VALUE 목록이다(주석 `#` 제외).
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
/**
|
|
18
|
+
* 검사 대상 — **모드를 가리지 않는** 공유 env 파일만. `.env.development` 같은 오버레이는
|
|
19
|
+
* 이름 자체가 모드라 NODE_ENV 를 적어도 새지 않는다(core loadDotEnv 가 모드별로만 얹는다).
|
|
20
|
+
*/
|
|
21
|
+
const SHARED_ENV_FILES = ['.env', '.env.local', '.env.example'];
|
|
22
|
+
/** 한 줄이 NODE_ENV 대입이면 그 값을 낸다. 주석·export 접두·따옴표를 흡수한다. */
|
|
23
|
+
export function nodeEnvAssignment(line) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
26
|
+
return undefined;
|
|
27
|
+
const m = trimmed.match(/^(?:export\s+)?NODE_ENV\s*=\s*(.*)$/);
|
|
28
|
+
if (!m)
|
|
29
|
+
return undefined;
|
|
30
|
+
// 값에서 인라인 주석·따옴표를 벗긴다(빈 값도 "설정됨" 으로 본다 — 빈 NODE_ENV 가
|
|
31
|
+
// vite 의 `!!process.env.NODE_ENV` 판정을 미설정으로 만들어 production 을 꽂는다).
|
|
32
|
+
const raw = m[1].replace(/\s+#.*$/, '').trim();
|
|
33
|
+
return raw.replace(/^['"]|['"]$/g, '');
|
|
34
|
+
}
|
|
35
|
+
/** 공유 `.env` 파일에 박힌 NODE_ENV 를 경고한다. */
|
|
36
|
+
export async function checkDotenvNodeEnv(cwd) {
|
|
37
|
+
const issues = [];
|
|
38
|
+
for (const name of SHARED_ENV_FILES) {
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = await readFile(join(cwd, name), 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue; // 없으면 통과(대부분의 프로젝트에 .env.local 은 없다).
|
|
45
|
+
}
|
|
46
|
+
const lines = text.split(/\r?\n/);
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const value = nodeEnvAssignment(lines[i]);
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
issues.push({
|
|
52
|
+
rule: 'dotenv-node-env',
|
|
53
|
+
level: 'warning',
|
|
54
|
+
file: name,
|
|
55
|
+
line: i + 1,
|
|
56
|
+
message: `${name} 에 NODE_ENV${value === '' ? '' : `=${value}`} 이(가) 설정돼 있습니다 — 이 파일은 개발·운영이 함께 읽습니다.\n` +
|
|
57
|
+
`모드는 **명령이 정합니다**: gaon dev = development · gaon serve = production(결정 430).\n` +
|
|
58
|
+
`여기에 값을 박으면 그 모드가 양쪽으로 샙니다 — production 이면 gaon dev 가 쿠키 Secure·` +
|
|
59
|
+
`dev secret 거부로 죽고, development 면 운영 serve 에서 프로덕션 안전장치(플레이스홀더 secret 거부·` +
|
|
60
|
+
`쿠키 Secure)가 통째로 꺼집니다(부팅은 green).\n` +
|
|
61
|
+
`→ ${name} 에서 NODE_ENV 줄을 지우세요. 특정 모드에서만 필요한 값은 .env.development / .env.production ` +
|
|
62
|
+
`오버레이에 두고, 일회성으로 바꿀 땐 명령 앞에 붙이세요: NODE_ENV=production gaon serve`,
|
|
63
|
+
detail: { file: name, line: i + 1, value },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { rule: 'dotenv-node-env', issues };
|
|
68
|
+
}
|
|
@@ -14,7 +14,7 @@ export declare const FIXERS: Partial<Record<DoctorRule, Fixer>>;
|
|
|
14
14
|
* 규칙별 fix 지원 여부 카탈로그. 리포트가 사용자에게 무엇이 자동 · 무엇이
|
|
15
15
|
* 수동 · 이유는 무엇인지 표시하는 데 쓴다(진단 = 수리 안내서 · §7.5.3).
|
|
16
16
|
*
|
|
17
|
-
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES`
|
|
17
|
+
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES` 30종을 **빠짐없이** 담는다 —
|
|
18
18
|
* fixer 가 없는 규칙도 `hasFixer:false` + 구체적 수동 안내로 명시한다. 항목이
|
|
19
19
|
* 빠지면 --fix 리포트가 그 규칙 위반에 대해 일반 문구("수동 수정 필요")만 내
|
|
20
20
|
* 사용자가 왜 자동이 안 되는지 알 수 없다. 전수성은 테스트가 고정한다
|
|
@@ -28,7 +28,7 @@ export const FIXERS = {
|
|
|
28
28
|
* 규칙별 fix 지원 여부 카탈로그. 리포트가 사용자에게 무엇이 자동 · 무엇이
|
|
29
29
|
* 수동 · 이유는 무엇인지 표시하는 데 쓴다(진단 = 수리 안내서 · §7.5.3).
|
|
30
30
|
*
|
|
31
|
-
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES`
|
|
31
|
+
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES` 30종을 **빠짐없이** 담는다 —
|
|
32
32
|
* fixer 가 없는 규칙도 `hasFixer:false` + 구체적 수동 안내로 명시한다. 항목이
|
|
33
33
|
* 빠지면 --fix 리포트가 그 규칙 위반에 대해 일반 문구("수동 수정 필요")만 내
|
|
34
34
|
* 사용자가 왜 자동이 안 되는지 알 수 없다. 전수성은 테스트가 고정한다
|
|
@@ -180,4 +180,9 @@ export const FIXER_CAPABILITIES = [
|
|
|
180
180
|
hasFixer: false,
|
|
181
181
|
note: '수동 · 앱마다 채널 이름을 분리(파일명 + 클라이언트 useChannel 인자 동시 변경)하거나, 의도적 공유면 정의를 shared/channels/ 하나로 옮기고 각 앱에서 재수출하세요 — 어느 쪽인지는 설계 판단이라 자동 정정하지 않습니다.',
|
|
182
182
|
},
|
|
183
|
+
{
|
|
184
|
+
rule: 'dotenv-node-env',
|
|
185
|
+
hasFixer: false,
|
|
186
|
+
note: '수동 · 공유 .env 에서 NODE_ENV 줄을 지우세요 — 모드는 명령이 정합니다(gaon dev=development · gaon serve=production · 결정 430). 지웠을 때 어떤 모드로 돌리려던 것인지는 사람이 알아야 해서 자동 정정하지 않습니다.',
|
|
187
|
+
},
|
|
183
188
|
];
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index' | 'column-casing' | 'model-filename' | 'page-filename' | 'auth-wiring' | 'ui-kit-wiring' | 'route-registration' | 'static-collision' | 'method-override' | 'csrf-wiring' | 'internal-anchor' | 'pageprops-destructure' | 'async-offload' | 'page-layout-breakpoint' | 'link-button-nesting' | 'seal-security' | 'schema-relations' | 'no-import-meta-env' | 'locale-parity' | 'render-return' | 'channel-collision';
|
|
1
|
+
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index' | 'column-casing' | 'model-filename' | 'page-filename' | 'auth-wiring' | 'ui-kit-wiring' | 'route-registration' | 'static-collision' | 'method-override' | 'csrf-wiring' | 'internal-anchor' | 'pageprops-destructure' | 'async-offload' | 'page-layout-breakpoint' | 'link-button-nesting' | 'seal-security' | 'schema-relations' | 'no-import-meta-env' | 'locale-parity' | 'render-return' | 'channel-collision' | 'dotenv-node-env';
|
|
2
2
|
export type DoctorLevel = 'passed' | 'warning' | 'error';
|
|
3
3
|
export interface DoctorCheck {
|
|
4
4
|
readonly rule: DoctorRule;
|
package/dist/doctor.d.ts
CHANGED
|
@@ -28,10 +28,10 @@ export { checkChannelCollision, reexportSpecifier } from './doctor/channel-colli
|
|
|
28
28
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
29
29
|
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
30
30
|
/**
|
|
31
|
-
* 실행할 검사 이름. 지정 없음(undefined) =
|
|
31
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 30개 모두.
|
|
32
32
|
*/
|
|
33
33
|
/**
|
|
34
|
-
* doctor 정적 검사
|
|
34
|
+
* doctor 정적 검사 30종의 정본 목록(§2.2). `--check=` 필터의 인정 집합도
|
|
35
35
|
* 이 배열을 단일 출처로 삼는다(parseDoctorChecks) — 새 규칙 추가 시 여기만
|
|
36
36
|
* 늘리면 실행·필터·타입이 함께 정합된다(손유지 중복 리스트 표류 방지).
|
|
37
37
|
*/
|
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성 · E-5 확장)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 30 검사를 조립한다:
|
|
5
5
|
* 1) response-mixing (errata E-3 §C · 라이브)
|
|
6
6
|
* 2) n-plus-one (errata E-4 (e))
|
|
7
7
|
* 3) dependency-direction (CLAUDE.md §5 · 4 규칙)
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* 27) locale-parity (결정 216 · 13차 W4 · 로케일 간 키 부분 누락 = fallback 조용 노출 경고)
|
|
32
32
|
* 28) render-return (결정 340 · this.render/redirect/json 호출만 하고 return 누락 = 무신호 204 경고)
|
|
33
33
|
* 29) channel-collision (§7 · 앱간 동명 채널 = 전역 subject·프레즌스 병합 error)
|
|
34
|
+
* 30) dotenv-node-env (결정 430 · 공유 .env 의 NODE_ENV = 모드 누출 경고)
|
|
34
35
|
*
|
|
35
36
|
* 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
|
|
36
37
|
* DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
|
|
@@ -72,6 +73,7 @@ import { checkLinkButtonNesting } from './doctor/link-button-nesting.js';
|
|
|
72
73
|
import { checkNoImportMetaEnv } from './doctor/no-import-meta-env.js';
|
|
73
74
|
import { checkLocaleParity } from './doctor/locale-parity.js';
|
|
74
75
|
import { checkChannelCollision } from './doctor/channel-collision.js';
|
|
76
|
+
import { checkDotenvNodeEnv } from './doctor/dotenv-node-env.js';
|
|
75
77
|
import { renderHuman, renderJson } from './doctor/reporter.js';
|
|
76
78
|
import { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
77
79
|
import { makeResult, } from './doctor/types.js';
|
|
@@ -103,10 +105,10 @@ export { checkChannelCollision, reexportSpecifier } from './doctor/channel-colli
|
|
|
103
105
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
104
106
|
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
105
107
|
/**
|
|
106
|
-
* 실행할 검사 이름. 지정 없음(undefined) =
|
|
108
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 30개 모두.
|
|
107
109
|
*/
|
|
108
110
|
/**
|
|
109
|
-
* doctor 정적 검사
|
|
111
|
+
* doctor 정적 검사 30종의 정본 목록(§2.2). `--check=` 필터의 인정 집합도
|
|
110
112
|
* 이 배열을 단일 출처로 삼는다(parseDoctorChecks) — 새 규칙 추가 시 여기만
|
|
111
113
|
* 늘리면 실행·필터·타입이 함께 정합된다(손유지 중복 리스트 표류 방지).
|
|
112
114
|
*/
|
|
@@ -140,6 +142,7 @@ export const ALL_RULES = [
|
|
|
140
142
|
'locale-parity',
|
|
141
143
|
'render-return',
|
|
142
144
|
'channel-collision',
|
|
145
|
+
'dotenv-node-env',
|
|
143
146
|
];
|
|
144
147
|
/**
|
|
145
148
|
* `gaon help` 이 doctor 한 줄에 요약할 규칙별 문구(§2.2 상세는 AGENTS). 타입이
|
|
@@ -177,6 +180,7 @@ export const RULE_SUMMARIES = {
|
|
|
177
180
|
'locale-parity': '로케일 커버리지',
|
|
178
181
|
'render-return': 'render return 누락',
|
|
179
182
|
'channel-collision': '앱간 동명 채널',
|
|
183
|
+
'dotenv-node-env': '.env NODE_ENV',
|
|
180
184
|
};
|
|
181
185
|
const CHECKERS = {
|
|
182
186
|
'response-mixing': checkResponseMixing,
|
|
@@ -208,6 +212,7 @@ const CHECKERS = {
|
|
|
208
212
|
'locale-parity': checkLocaleParity,
|
|
209
213
|
'render-return': checkRenderReturn,
|
|
210
214
|
'channel-collision': checkChannelCollision,
|
|
215
|
+
'dotenv-node-env': checkDotenvNodeEnv,
|
|
211
216
|
};
|
|
212
217
|
/**
|
|
213
218
|
* 규칙을 순서대로 실행해 RuleReport[] 를 낸다. 규칙 하나가 크래시해도 나머지는
|
package/dist/index.js
CHANGED
|
@@ -151,7 +151,7 @@ function renderHelp(version = VERSION) {
|
|
|
151
151
|
* 지정 없음(undefined) = 5 검사 모두 실행. 알 수 없는 이름은 무시(안전).
|
|
152
152
|
*/
|
|
153
153
|
export function parseDoctorChecks(argv) {
|
|
154
|
-
// 인정 집합은 doctor.ts 의 ALL_RULES(정본
|
|
154
|
+
// 인정 집합은 doctor.ts 의 ALL_RULES(정본 30종)를 단일 출처로 쓴다 — 과거
|
|
155
155
|
// 손유지 9종 리스트가 뒤처져 --check=seal-security 같은 16종이 조용히 무시되고
|
|
156
156
|
// 전체 검사로 되돌아가던 표류를 근본 차단한다(결정 168).
|
|
157
157
|
const isKnown = (s) => ALL_RULES.includes(s);
|
|
@@ -172,7 +172,7 @@ export function parseDoctorChecks(argv) {
|
|
|
172
172
|
}
|
|
173
173
|
// 결정 411: 모르는 이름은 여전히 무시하되(안전 방향 — 전체 검사로 넓어짐) **조용히**
|
|
174
174
|
// 넘기지 않는다. 오타 하나가 "그 검사만 돌렸다" 는 착각으로 이어지고, 전부 오타면
|
|
175
|
-
//
|
|
175
|
+
// 30종 전체가 돌아가 선택 실행 의도가 통째로 사라진다.
|
|
176
176
|
if (unknown.length > 0) {
|
|
177
177
|
process.stderr.write(` ! 알 수 없는 검사 이름 무시: ${unknown.join(", ")}\n` +
|
|
178
178
|
` → 지원 이름은 gaon doctor --json 의 rule 값 또는 gaon help 참고` +
|
|
@@ -472,7 +472,7 @@ export function runCli(argv, opts = {}) {
|
|
|
472
472
|
});
|
|
473
473
|
return;
|
|
474
474
|
}
|
|
475
|
-
// `gaon doctor` — 정적 검사(M9-E ·
|
|
475
|
+
// `gaon doctor` — 정적 검사(M9-E · 30 검사 · ALL_RULES 단일 출처). --check=<이름>[,<이름>...] 로
|
|
476
476
|
// 선택 실행, --json 은 자동화 파싱용.
|
|
477
477
|
// exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
|
|
478
478
|
if (argv[0] === "doctor") {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · NODE_ENV 기본값 (결정 430)
|
|
3
|
+
*
|
|
4
|
+
* **모드는 명령이 정한다** — `gaon dev` = development · `gaon serve` = production.
|
|
5
|
+
* 사용자가 명시한 값(셸 env·`.env`)은 항상 존중한다(로컬 프로덕션 확인 `NODE_ENV=production
|
|
6
|
+
* gaon dev` 같은 정당 케이스). 부팅 진입점이 loadDotEnv 직후, 다른 배선보다 먼저 부른다.
|
|
7
|
+
*
|
|
8
|
+
* 이게 없으면 두 방향으로 샌다:
|
|
9
|
+
* · dev — `gaon dev` 가 vite 를 **자기 프로세스 안에서** 부르는데(dev/build.ts), vite 의
|
|
10
|
+
* resolveConfig(build)가 NODE_ENV 미설정 시 process.env.NODE_ENV 에 'production' 을 꽂는다.
|
|
11
|
+
* 이후 파일 저장으로 재기동된 serve·work 자식이 오염된 production 을 상속해 prod 게이트에
|
|
12
|
+
* 걸려 즉사한다(최초 자식은 vite 빌드 전에 떠서 멀쩡 — 저장 한 번에 죽는 split-brain).
|
|
13
|
+
* · serve — NODE_ENV 미설정 운영 배포는 어느 쪽도 아니어서 prod 게이트(세션 dev 플레이스홀더
|
|
14
|
+
* secret 거부·쿠키 Secure 기본)가 **안 걸린 채** 조용히 떴다(fail-loud 우회 구멍).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* NODE_ENV 가 비어 있으면 `mode` 로 채운다. 이미 값이 있으면 건드리지 않는다.
|
|
18
|
+
*
|
|
19
|
+
* 빈 문자열은 **미설정으로 본다** — `.env` 의 `NODE_ENV=` 만 적힌 흔한 실수를 걸러야 한다
|
|
20
|
+
* (core env.ts `raw()` 와 같은 규약). `??=` 로는 '' 가 통과하는데, vite 도 `!!process.env.NODE_ENV`
|
|
21
|
+
* 로 판정하므로 '' 를 남겨두면 결국 vite 가 production 을 꽂아 같은 사고가 난다.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defaultNodeEnv(mode: 'development' | 'production'): void;
|
package/dist/nodeEnv.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · NODE_ENV 기본값 (결정 430)
|
|
3
|
+
*
|
|
4
|
+
* **모드는 명령이 정한다** — `gaon dev` = development · `gaon serve` = production.
|
|
5
|
+
* 사용자가 명시한 값(셸 env·`.env`)은 항상 존중한다(로컬 프로덕션 확인 `NODE_ENV=production
|
|
6
|
+
* gaon dev` 같은 정당 케이스). 부팅 진입점이 loadDotEnv 직후, 다른 배선보다 먼저 부른다.
|
|
7
|
+
*
|
|
8
|
+
* 이게 없으면 두 방향으로 샌다:
|
|
9
|
+
* · dev — `gaon dev` 가 vite 를 **자기 프로세스 안에서** 부르는데(dev/build.ts), vite 의
|
|
10
|
+
* resolveConfig(build)가 NODE_ENV 미설정 시 process.env.NODE_ENV 에 'production' 을 꽂는다.
|
|
11
|
+
* 이후 파일 저장으로 재기동된 serve·work 자식이 오염된 production 을 상속해 prod 게이트에
|
|
12
|
+
* 걸려 즉사한다(최초 자식은 vite 빌드 전에 떠서 멀쩡 — 저장 한 번에 죽는 split-brain).
|
|
13
|
+
* · serve — NODE_ENV 미설정 운영 배포는 어느 쪽도 아니어서 prod 게이트(세션 dev 플레이스홀더
|
|
14
|
+
* secret 거부·쿠키 Secure 기본)가 **안 걸린 채** 조용히 떴다(fail-loud 우회 구멍).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* NODE_ENV 가 비어 있으면 `mode` 로 채운다. 이미 값이 있으면 건드리지 않는다.
|
|
18
|
+
*
|
|
19
|
+
* 빈 문자열은 **미설정으로 본다** — `.env` 의 `NODE_ENV=` 만 적힌 흔한 실수를 걸러야 한다
|
|
20
|
+
* (core env.ts `raw()` 와 같은 규약). `??=` 로는 '' 가 통과하는데, vite 도 `!!process.env.NODE_ENV`
|
|
21
|
+
* 로 판정하므로 '' 를 남겨두면 결국 vite 가 production 을 꽂아 같은 사고가 난다.
|
|
22
|
+
*/
|
|
23
|
+
export function defaultNodeEnv(mode) {
|
|
24
|
+
if (!process.env.NODE_ENV)
|
|
25
|
+
process.env.NODE_ENV = mode;
|
|
26
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -18,6 +18,7 @@ import { availableParallelism } from 'node:os';
|
|
|
18
18
|
import { loadDotEnv } from '@gaonjs/core';
|
|
19
19
|
import { loadGaonConfig, wireGaon, findConfigPath } from '@gaonjs/config';
|
|
20
20
|
import { registerTsResolve } from './tsResolve.js';
|
|
21
|
+
import { defaultNodeEnv } from './nodeEnv.js';
|
|
21
22
|
import { parsePort } from './port.js';
|
|
22
23
|
import { computeHealth, DEV_HEALTH_PATH } from './dev/health.js';
|
|
23
24
|
import { createReforkSupervisor, reforkPolicyFromEnv } from './cluster.js';
|
|
@@ -152,6 +153,14 @@ export async function runServeCommand(opts = {}) {
|
|
|
152
153
|
// .ts 상대 import 해석기 등록 (사용자 gaon.config.ts, apps/* 로드에 필요).
|
|
153
154
|
registerTsResolve();
|
|
154
155
|
loadDotEnv(cwd);
|
|
156
|
+
// 결정 430: 모드는 명령이 정한다 — `gaon serve` 는 production 이 기본이고, `gaon dev` 가
|
|
157
|
+
// 스폰한 자식(--dev)만 development. 이전엔 NODE_ENV 미설정 serve 가 어느 쪽도 아니어서
|
|
158
|
+
// 운영 배포에서 prod 게이트가 통째로 **안 걸렸다**: 세션 dev 플레이스홀더 secret 거부
|
|
159
|
+
// (결정 255)·쿠키 Secure 기본(결정 295)·락 백엔드 in-memory 폴백 차단(결정 88①)이 전부
|
|
160
|
+
// `NODE_ENV === 'production'` 판정에 걸려 있다 — fail-loud 를 무설정으로 우회하던 구멍.
|
|
161
|
+
// dev 자식은 부모의 development 를 스냅샷으로 상속하므로 여기서 다시 결정되지 않는다.
|
|
162
|
+
const explicitNodeEnv = process.env.NODE_ENV || undefined;
|
|
163
|
+
defaultNodeEnv(opts.dev ? 'development' : 'production');
|
|
155
164
|
const emit = (e) => {
|
|
156
165
|
if (json)
|
|
157
166
|
process.stdout.write(JSON.stringify(e) + '\n');
|
|
@@ -201,7 +210,10 @@ export async function runServeCommand(opts = {}) {
|
|
|
201
210
|
// 수정이 조용히 반영 안 되는 혼란(첫 실사용 관측)을 막으려 한 줄 안내한다.
|
|
202
211
|
// dev 자식(gaon dev · opts.dev)은 이미 감시하므로 그때는 안내하지 않고,
|
|
203
212
|
// production 은 운영 로그 소음을 막으려 출력하지 않는다. json 은 파싱 안전상 제외.
|
|
204
|
-
|
|
213
|
+
// 결정 430: 판정은 **명시된** NODE_ENV 로 한다 — 위에서 채운 기본값(production)으로
|
|
214
|
+
// 보면 로컬에서 그냥 `gaon serve` 한 사람(이 안내가 가장 필요한 대상)에게서 안내가
|
|
215
|
+
// 사라진다. 운영은 NODE_ENV=production 을 명시하므로 침묵 의도는 그대로다.
|
|
216
|
+
if (!json && !opts.dev && explicitNodeEnv !== 'production') {
|
|
205
217
|
process.stdout.write(' ℹ serve 는 빌드된 번들을 서빙하며 코드 변경을 감시하지 않습니다 → 개발 중이면 `gaon dev` 를 쓰세요.\n');
|
|
206
218
|
}
|
|
207
219
|
}
|
|
@@ -113,7 +113,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
113
113
|
컬럼명 · 스키마 파일 ↔ 테이블 ↔ `tables.d.ts` 키 변환 규칙)은
|
|
114
114
|
`agents/data.md` "DB 네이밍" 표가 정본이다 — 먼저 읽는다.
|
|
115
115
|
|
|
116
|
-
### 2.2 `gaon doctor` 검사
|
|
116
|
+
### 2.2 `gaon doctor` 검사 30종
|
|
117
117
|
|
|
118
118
|
1. `response-mixing` — 한 액션 안 render/JSON/redirect 혼용 (E-3)
|
|
119
119
|
2. `n-plus-one` — include 미사용 · loop 안 관계 호출 (E-4)
|
|
@@ -144,6 +144,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
144
144
|
27. `locale-parity` — `locales/` 의 로케일 간 키 부분 누락 = **경고**. 어떤 키가 특정 로케일에만 빠지면 `messages.d.ts`(기준 로케일 기준)는 컴파일을 통과하고, 런타임에 그 로케일 사용자는 fallback(대개 다른 언어) 번역을 조용히 본다. 검사가 로케일 간 키 diff 를 계산해 빠진 파일·키를 짚는다(`--json` 은 `detail.missing` 으로 구조화). 로케일이 0·1개면 무소음 (결정 216 · `agents/i18n.md`)
|
|
145
145
|
28. `render-return` — 액션이 `this.render`/`this.redirect`/`this.json` 을 호출만 하고 `return` 하지 않음 = 응답이 버려져 조용히 204(백지) — `return this.render(...)` 로 고치라 (결정 340 · 경고)
|
|
146
146
|
29. `channel-collision` — 두 앱이 **같은 이름의 채널**을 각각 정의 = **에러**. 채널 이름은 전역이다(브로드캐스트 subject `gaon.chan.<이름>`·프레즌스 키에 앱 프리픽스 없음) — 한 앱의 broadcast 가 다른 앱 연결로 팬아웃되고 접속자 목록이 병합되며, 두 정의의 `authorize` 가 갈리면 공개 쪽 규칙으로 메시지가 샌다. 앱마다 이름을 분리하거나(클라이언트 `useChannel` 인자도 함께), 일부러 공유하는 채널이면 정의를 `shared/channels/<이름>.ts` 하나에 두고 각 앱 채널 파일에서 재수출하라(재수출은 통과 · 정의 하나 = 인가 규칙 하나) — 잡·리스너의 동명 등록 throw(결정 271)와 같은 계열의 정적 검사 (`agents/realtime.md` §2)
|
|
147
|
+
30. `dotenv-node-env` — 공유 `.env`(·`.env.local`·`.env.example`)에 `NODE_ENV` 가 설정됨 = **경고**. **모드는 명령이 정한다** — `gaon dev` = development · `gaon serve` = production(결정 430). 이 파일들은 개발·운영이 함께 읽으므로 값을 박으면 모드가 양쪽으로 샌다: `production` 이면 `gaon dev` 가 쿠키 Secure·dev 플레이스홀더 secret 거부로 죽고, `development` 면 운영 `gaon serve` 에서 프로덕션 안전장치(플레이스홀더 secret 거부·쿠키 Secure·락 in-memory 폴백 차단)가 통째로 꺼진다(**부팅은 green, 보안만 꺼짐**). `.env` 에서 그 줄을 지우고, 모드별 값이 필요하면 `.env.development`/`.env.production` 오버레이에, 일회성이면 명령 앞에 붙인다(`NODE_ENV=production gaon serve`) — 모드별 오버레이 파일은 검사 대상이 아니다 (결정 430)
|
|
147
148
|
|
|
148
149
|
## 3. 로직 배치 One Way 판단표
|
|
149
150
|
|
|
@@ -204,7 +205,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
204
205
|
```bash
|
|
205
206
|
gaon check # .gaon 재생성 → typecheck + vue-tsc + build + doctor (기본 포함 · --no-doctor 로 뺌 · 결정 157)
|
|
206
207
|
gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
|
|
207
|
-
gaon doctor # 정적 검사
|
|
208
|
+
gaon doctor # 정적 검사 30종 (§2.2)
|
|
208
209
|
```
|
|
209
210
|
|
|
210
211
|
### 4.1 CLI 명령 (전 명령 `--json` 지원)
|
|
@@ -232,6 +233,30 @@ gaon doctor # 정적 검사 29종 (§2.2)
|
|
|
232
233
|
개발 중이면 `gaon dev`(감시·재시작·`.gaon` 재생성 통합)를 쓴다. `serve` 는
|
|
233
234
|
비-production 부팅 시 이 안내를 한 줄 출력한다.
|
|
234
235
|
|
|
236
|
+
**`NODE_ENV` — 모드는 명령이 정한다** (결정 430). `gaon dev` 는 `development`,
|
|
237
|
+
`gaon serve` 는 `production` 을 기본으로 잡는다(이미 설정돼 있으면 그 값을 존중 —
|
|
238
|
+
`NODE_ENV=production gaon serve`·`NODE_ENV=production gaon dev` 는 그대로 동작).
|
|
239
|
+
**`.env` 에 `NODE_ENV` 를 쓰지 않는다** — `.env`·`.env.local`·`.env.example` 은 개발과
|
|
240
|
+
운영이 함께 읽는 파일이라 한쪽 모드가 반대쪽으로 샌다(운영이 development 로 뜨면
|
|
241
|
+
플레이스홀더 secret 거부·쿠키 Secure 같은 프로덕션 안전장치가 부팅 green 인 채로 꺼진다).
|
|
242
|
+
모드별로 다른 값이 필요하면 `.env.development`/`.env.production` 오버레이에 둔다
|
|
243
|
+
(doctor `dotenv-node-env` 가 검사한다 · §2.2).
|
|
244
|
+
|
|
245
|
+
> **이행 안내(동작 변경)** — 종전에는 `NODE_ENV` 를 안 주면 `gaon serve` 가 **어느 모드도
|
|
246
|
+
> 아닌 상태**로 떠서 프로덕션 안전장치가 하나도 안 걸렸다. 이제는 `production` 이므로,
|
|
247
|
+
> **로컬에서 `gaon build && gaon serve` 로 확인하던 사람**은 다음 둘 중 하나가 보인다:
|
|
248
|
+
> ① 세션 secret 이 스캐폴드 플레이스홀더면 **부팅이 exit 1** 로 거부된다(결정 255)
|
|
249
|
+
> ② 떠도 세션 쿠키가 `Secure` 라 **비-TLS(http://localhost) 로그인이 안 붙는다**(결정 295).
|
|
250
|
+
> 둘 다 "운영에서 켜졌어야 할 게 이제 켜진" 것이다 — 끄지 말고 아래로 옮긴다:
|
|
251
|
+
> - **로컬 확인이 목적이면 `gaon dev`**(또는 `gaon serve --dev`). 이게 정답 경로다.
|
|
252
|
+
> - **로컬에서 프로덕션 빌드를 그대로 보고 싶으면** 실 `SESSION_SECRET` 을 준다:
|
|
253
|
+
> `SESSION_SECRET=$(openssl rand -hex 32) gaon serve`. 쿠키 `Secure` 때문에 로그인까지
|
|
254
|
+
> 봐야 한다면 TLS 종단을 앞에 두거나 그 확인만 `gaon dev` 로 한다.
|
|
255
|
+
>
|
|
256
|
+
> **운영 로그 파이프라인 주의** — `gaon hub` 의 치명 오류는 이제 루트 로거로 나간다(결정 432).
|
|
257
|
+
> pino 기본 목적지가 **stdout** 이라, `2>` 로 stderr 만 수집하던 설정은 이 메시지를 놓친다
|
|
258
|
+
> (로거 확보에 실패한 경우에만 종전대로 stderr 로 떨어진다). stdout 을 함께 수집한다.
|
|
259
|
+
|
|
235
260
|
**스케일링 구분** — `serve --workers N`(한 포트 · node:cluster 수직) vs 웹 인스턴스
|
|
236
261
|
여러 대(각각 다른 `PORT` · 허브 뒤 수평) vs `work`(포트 없음 · 프로세스만)는 서로
|
|
237
262
|
다르다. 워커 다중화 시 스케줄 발행=단일 리더 · 소비=워커 분산 계약은
|
|
@@ -86,7 +86,7 @@ Gaon 프레임웍 문서: https://gaonjs.dev
|
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
gaon check # .gaon 재생성 → 타입검사+build+doctor (CI 한 번에 · --no-doctor 로 doctor 뺌)
|
|
89
|
-
gaon doctor # 정적 검사
|
|
89
|
+
gaon doctor # 정적 검사 30종 (상세 AGENTS §2.2)
|
|
90
90
|
npm test # Vitest · DB 테스트는 실 Docker 필수 (§9)
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -160,10 +160,14 @@ seal 앱 응답에만 `script-src` 에 `'wasm-unsafe-eval'` 을 **자동 주입*
|
|
|
160
160
|
- **알고리즘**: AES-256-GCM(12-byte nonce · 16-byte tag) + nibble-swap XOR(0x5A) + base64 · 키 유도 =
|
|
161
161
|
`SHA256(hex(HMAC-SHA256(masterSecret, "domain:path:uaSlice:timestamp")))` · per-frame keying(userId 미포함).
|
|
162
162
|
- **replay 방어**: AES-GCM 12-byte nonce 를 `setIfNotExists`(Redis SETNX 대응) 캐시 + timestamp drift(±60s)로
|
|
163
|
-
차단. **HTTP 경로는 nonce 검사가 항상 배선된다 (결정 223)** —
|
|
164
|
-
인스턴스 안전), 없으면 **in-memory 폴백(단일 인스턴스 전용)**을 쓰고 부팅 시 경고한다(nonce 검사가
|
|
165
|
-
사라지지 않는다). **
|
|
166
|
-
|
|
163
|
+
차단. **HTTP 경로는 nonce 검사가 항상 배선된다 (결정 223)** — Redis 핸들이 있으면 공유 store 를 재사용하고
|
|
164
|
+
(멀티 인스턴스 안전), 없으면 **in-memory 폴백(단일 인스턴스 전용)**을 쓰고 부팅 시 경고한다(nonce 검사가
|
|
165
|
+
조용히 사라지지 않는다). **Redis 핸들은 두 통로에서 온다 (결정 431)**: ① 앱의 세션 Redis(`app.config.ts`
|
|
166
|
+
의 `session`) ② 없으면 **프로세스 공용 Redis** — `gaon.config.ts` 의 `redis: { url: env('REDIS_URL') }`.
|
|
167
|
+
즉 **세션을 쓰지 않는 seal 앱(JWT API 앱)도 `REDIS_URL` 만 구성하면 공유 nonce store 를 탄다** — 세션을
|
|
168
|
+
억지로 붙일 필요가 없다. **in-memory 는 프로세스별 격리라 멀티 워커(`--workers`·`WEB_CONCURRENCY>1`)·멀티
|
|
169
|
+
서버에서 replay 를 완전히 막지 못하므로, 프로덕션 멀티 인스턴스는 `gaon.config.ts` 에 `redis` 를 구성한다**
|
|
170
|
+
(그러면 부팅 경고도 사라진다 — 경고가 남아 있다는 건 두 통로가 모두 비었다는 뜻이다). WS 기본은 drift 윈도우
|
|
167
171
|
(고빈도라 프레임마다 SETNX 는 비용 과다 · 엄격 nonce 는 옵션 주입).
|
|
168
172
|
- **허브(`gaon hub`)는 손대지 않는다** — 봉인/개봉은 각 웹서버의 소켓 경계에서만. 타 서버 접속자의
|
|
169
173
|
UA·ts 컨텍스트가 없어 허브가 프레임을 복호할 수 없는 것은 구조적 필연(설계상) · 허브·NATS 내부는 평문.
|
|
@@ -228,6 +232,7 @@ export default controller({
|
|
|
228
232
|
- **결정 125** — **Inertia 네비게이션 평문 P0** 수정: 봉인 대상 판별기(`isSealTarget`)에 `X-Inertia: true` 를 편입. Inertia GET 방문은 `Accept: text/html` 로 와 application/json 이 없어 자동 면제되던 탓에 응답 props 가 평문으로 새어나갔다(클라 인터셉터는 시그널을 붙였으나 서버가 봉인 안 함). 네비게이션 봉인 e2e 를 seal blocking 게이트에 편입(실 vite+chromium · wire 봉인/`?q=` 왕복 단언).
|
|
229
233
|
- **결정 222** — **클라 WS 수신 fail-open P1** 수정: 클라 `wsDecode`(client.ts)가 `P:` 평문·무prefix 프레임을 throw 없이 원문 통과시켜, 서버는 requireDecrypt 로 거부하는데 클라만 주입된 평문을 소비하던 봉인 파괴. `wsDecode` 를 서버 `SealWsTerminator` 와 대칭으로 만들어 `E:` 만 개봉·`P:`/무prefix 거부. `useChannel` 은 개봉 실패를 조용히 드롭하지 않고 소켓을 **4500 종료**(서버 대칭) + 콘솔 명시 + 재연결 안 함.
|
|
230
234
|
- **결정 223** — **HTTP replay Redis 없으면 조용히 off + 허위 주석 P1** 수정: `normalizeSealConfig` 이 nonceStore 없으면 `replay=null` 로 두어 nonce 검사가 사라지고 drift(±60s)만 남아 60초 내 재전송이 통과했다(`sealBridge` 주석은 "in-memory 폴백" 이라 거짓 단언 — `MemoryNonceStore` 는 export 만·미배선). HTTP replay 를 **항상 배선**한다 — Redis 있으면 재사용(멀티 인스턴스 안전), 없으면 in-memory 폴백(단일 인스턴스 전용) + 부팅 경고. **기각: 부팅 throw(옵션 A)** — 기본 배포가 워커 1(CLAUDE 규칙 6)이라 단일 인스턴스 in-memory 가 정상 경로인데 throw 는 dev·단일 인스턴스 seal 앱을 깨고 문서(§4 "in-memory 폴백 = 단일 인스턴스 전용")와 상충. 폴백+경고가 비파괴적·정본 정합.
|
|
235
|
+
- **결정 431** — **세션 없는 seal 앱의 nonce store 폴백**: seal 이 `spec.session?.redis` 만 봐서, 세션을 쓰지 않는 seal 앱(JWT API 앱이 정확히 이 모양)은 `REDIS_URL` 을 구성해 두고도 in-memory nonce 로 떨어졌다 — 멀티 워커·멀티 인스턴스에서 replay 방어가 프로세스별로 갈리는 보안 갭(부팅 green·무신호). `createApp` 에 프로세스 공용 `redis` 옵션을 두고(부트스트랩이 `gaon.config` 의 redis 를 전달) `세션 redis ?? 공용 redis` 로 폴백한다. 폴백에도 없을 때만 남는 경고 문구도 실제 수리 경로로 정정 — 옛 문구 "세션 Redis 를 구성하세요" 는 세션이 필요 없는 API 앱에 틀린 처방이었다. **기각: 세션 강제(앱마다 session 배선 요구)** — API 앱에 쓰지도 않는 세션 저장소를 켜게 하는 역행이고 앱별 세션 분리 원칙과 충돌. **기각: seal 이 자기 Redis 커넥션을 새로 연다** — 프로세스당 커넥션 1개 규약(결정 202)을 깨고 종료 소유권이 갈린다.
|
|
231
236
|
- **결정 224** — **최초 문서 data-page 평문 유출 P0** 수정: 서버가 주입한 **진짜** data-page 만 `data-gaon-seal-target` sentinel 로 특정해 봉인하고, 봉인 후에도 평문 data-page 잔재가 남으면 fail-closed 로 throw(§2·§5). seal 풀스택/브라우저 e2e 를 blocking 배포 게이트에 편입.
|
|
232
237
|
- **결정 248** — seal/web **에러 핸들러 단일화**(FSTWRN004): seal 플러그인은 자기 `setErrorHandler` 를 등록하지 않고(`installErrorHandler:false`) web 스코프가 하나만 등록한다. **seal 배선 코드는 자체 에러 핸들러를 달지 말 것**(중복 등록 = FSTWRN004 · 아키텍처 경계 · §4).
|
|
233
238
|
- **결정 354** — seal 백로그 2건: ① **prefix 앱 except 무력** 수정 — except 글롭·기본 헬스 제외를 **앱 상대 경로**로도 매칭(전체 경로 매칭 병행 · 하위 호환). 배선부(web)가 앱 prefix 를 normalizeSealConfig 로 전달. ② **strictQuery 옵션** 신설 — 봉인 강제 요청의 평문 쿼리를 403 `plaintext_query` 로 거부(기본 off — 직접 URL 인바운드가 흔해 기본 강제는 정당한 요청을 깬다). 클라 인터셉터 except 전파는 DEFER(함정 11).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
33
|
"typescript": "^5.9.0",
|
|
34
34
|
"vite": "^7.0.0",
|
|
35
|
-
"@gaonjs/
|
|
36
|
-
"@gaonjs/
|
|
37
|
-
"@gaonjs/
|
|
35
|
+
"@gaonjs/async": "0.18.1",
|
|
36
|
+
"@gaonjs/i18n": "0.3.0",
|
|
37
|
+
"@gaonjs/config": "0.25.0",
|
|
38
|
+
"@gaonjs/data": "0.25.2",
|
|
39
|
+
"@gaonjs/web": "0.30.0",
|
|
38
40
|
"@gaonjs/mail": "0.5.0",
|
|
39
|
-
"@gaonjs/
|
|
40
|
-
"@gaonjs/web": "0.29.0",
|
|
41
|
-
"@gaonjs/i18n": "0.3.0"
|
|
41
|
+
"@gaonjs/core": "0.2.4"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""
|