@gaonjs/cli 0.47.0 → 0.52.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/check.d.ts +1 -1
- package/dist/commands/check.js +1 -1
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +4 -3
- package/dist/db/journal.js +21 -10
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +3 -3
- package/dist/db/replay.js +2 -2
- package/dist/db/status.js +13 -3
- package/dist/dev.d.ts +6 -4
- package/dist/dev.js +9 -4
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +6 -1
- package/dist/doctor/locale-parity.js +4 -1
- package/dist/doctor/render-return.d.ts +11 -0
- package/dist/doctor/render-return.js +143 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.js +16 -5
- package/dist/generate.d.ts +20 -1
- package/dist/generate.js +120 -21
- package/dist/hub.js +2 -0
- package/dist/i18n-config.d.ts +12 -0
- package/dist/i18n-config.js +95 -0
- package/dist/index.js +41 -12
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +5 -2
- package/dist/templates/auth/Dashboard.vue.tpl +3 -2
- package/dist/templates/auth/Login.vue.tpl +3 -5
- package/dist/templates/auth/Signup.vue.tpl +3 -5
- package/dist/templates/auth/jwt.app.config.ts.tpl +18 -0
- package/dist/templates/auth/jwt.auth.wiring.ts.tpl +16 -0
- package/dist/templates/auth/jwt.routes.ts.tpl +7 -0
- package/dist/templates/auth/jwt.session.controller.ts.tpl +36 -0
- package/dist/templates/project/AGENTS.md.tpl +3 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/agents/async.md.tpl +35 -8
- package/dist/templates/project/agents/data.md.tpl +113 -49
- package/dist/templates/project/agents/frontend.md.tpl +15 -4
- package/dist/templates/project/agents/i18n.md.tpl +5 -2
- package/dist/templates/project/agents/mail.md.tpl +2 -1
- package/dist/templates/project/agents/realtime.md.tpl +16 -6
- package/dist/templates/project/agents/seal.md.tpl +6 -3
- package/dist/templates/project/agents/security.md.tpl +37 -19
- package/dist/templates/project/agents/storage.md.tpl +6 -5
- package/dist/templates/project/agents/web.md.tpl +40 -22
- package/package.json +6 -6
|
@@ -168,35 +168,32 @@ export default controller({
|
|
|
168
168
|
①이 검증까지 주므로 **모델이 있으면 ①을 먼저 고른다**. ②는 로그인 폼처럼
|
|
169
169
|
전용 테이블이 없는 입력의 탈출구다.
|
|
170
170
|
|
|
171
|
-
**멀티파트 업로드 + CSRF —
|
|
171
|
+
**멀티파트 업로드 + CSRF — 서버 검사는 `x-csrf-token` 헤더로만 (결정 133 · 부착은 자동 · 결정 342):**
|
|
172
172
|
|
|
173
|
-
`this.file()` 업로드(멀티파트)의 CSRF
|
|
173
|
+
`this.file()` 업로드(멀티파트)의 CSRF 검사는 **`x-csrf-token` 헤더**만 본다.
|
|
174
174
|
멀티파트는 `parts()` 스트리밍이라 CSRF 검사(preHandler) 시점에 **바디가 아직
|
|
175
175
|
파싱되지 않아** 폼 필드 `_csrf` 가 검사에 잡히지 않는다(구조적 한계 · 디스패처가
|
|
176
|
-
handler 안에서 파싱).
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`application/json` · `multipart/form-data` 뿐이라
|
|
180
|
-
|
|
176
|
+
handler 안에서 파싱). **결정 342 이후 헤더 부착은 프레임웍 자동**이다 —
|
|
177
|
+
`useForm`/`router` 의 상태 변경 제출(멀티파트 포함)에 최신 토큰이
|
|
178
|
+
`X-CSRF-Token` 으로 자동 실리므로 업로드 폼도 손으로 헤더를 넘기지 않는다.
|
|
179
|
+
(참고: 지원 Content-Type 은 `application/json` · `multipart/form-data` 뿐이라
|
|
180
|
+
`x-www-form-urlencoded` 로 폼을 보내면 415 로 거부된다 · `inertia.ts` · §아래 415.)
|
|
181
181
|
|
|
182
182
|
```vue
|
|
183
183
|
<script setup lang="ts">
|
|
184
|
-
import { useForm
|
|
185
|
-
const shared = useShared() // csrf 는 자동 주입 공유 prop (결정 116)
|
|
184
|
+
import { useForm } from 'gaonjs/vue'
|
|
186
185
|
const form = useForm({ avatar: null as File | null })
|
|
187
186
|
|
|
188
187
|
function submit() {
|
|
189
|
-
// 파일이 있으면 multipart —
|
|
190
|
-
form.post('/uploads', { headers: { 'x-csrf-token': shared.csrf } })
|
|
188
|
+
form.post('/uploads') // 파일이 있으면 multipart — CSRF 헤더는 자동(결정 342)
|
|
191
189
|
}
|
|
192
190
|
</script>
|
|
193
191
|
```
|
|
194
192
|
|
|
195
|
-
- **알려진 함정:** 업로드
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
예시). 멀티파트일 때만 헤더가 유일 경로다.
|
|
193
|
+
- **알려진 함정:** 업로드 폼에 `useForm({ avatar, _csrf: ... })` 처럼 `_csrf` 를
|
|
194
|
+
바디 필드로 넣어도 멀티파트에선 **검사 시점에 없어 무의미**하다(자동 헤더가
|
|
195
|
+
실질 경로). 커스텀 fetch 업로더처럼 `useForm`/`router` 를 우회하면 자동 부착이
|
|
196
|
+
없다 — `readCsrfToken()`(gaonjs/vue)으로 토큰을 읽어 헤더에 직접 실어라.
|
|
200
197
|
|
|
201
198
|
### 4. 데이터 경로 판단 — 루트 판단표가 정본
|
|
202
199
|
|
|
@@ -213,14 +210,13 @@ redirect 로 처리한다 — 전체 페이지 리로드도, 별도 REST 엔드
|
|
|
213
210
|
```ts
|
|
214
211
|
// 로그인 폼 — 제출은 Inertia SPA 방식, 서버는 redirect 로 답한다.
|
|
215
212
|
// pageProps 는 반응형 — 변수로 받아 props.x 로 접근한다(구조분해 금지 · 결정 99).
|
|
216
|
-
//
|
|
213
|
+
// CSRF 토큰은 프레임웍이 자동 부착한다(결정 342) — _csrf 바디도, 수동 헤더도 없다.
|
|
217
214
|
const props = pageProps<'web:session#new'>()
|
|
218
|
-
const
|
|
219
|
-
const form = useForm({ email: '', password: '', _csrf: shared.csrf })
|
|
215
|
+
const form = useForm({ email: '', password: '' })
|
|
220
216
|
// <form @submit.prevent="form.post('/session')"> · 실패 시 {{ props.error }} 가 반응형으로 갱신
|
|
221
217
|
|
|
222
|
-
// HTML <form> 이 못 보내는 메서드(DELETE 등)는 router 로
|
|
223
|
-
router.delete('/session'
|
|
218
|
+
// HTML <form> 이 못 보내는 메서드(DELETE 등)는 router 로 보낸다(CSRF 자동).
|
|
219
|
+
router.delete('/session')
|
|
224
220
|
```
|
|
225
221
|
|
|
226
222
|
`?_method=DELETE` 같은 우회는 **서버가 해석하지 않는다** — POST 로 나가
|
|
@@ -489,6 +485,21 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
|
|
|
489
485
|
|
|
490
486
|
API 앱(JWT)은 세션 대신 `this.jwt.issue(user)` / `this.jwt.refresh(token)` 를 쓴다.
|
|
491
487
|
|
|
488
|
+
- **JWT 하드닝 (결정 337 · 세션 결정 255 와 대칭).** JWT secret 은 **32자 이상**이
|
|
489
|
+
부팅 요건이고(미만 = 부팅 에러), 운영(NODE_ENV=production)에서 dev 폴백/
|
|
490
|
+
플레이스홀더(`dev-only-…`·`change-me…`)가 남아 있으면 부팅이 확정 종료된다.
|
|
491
|
+
검증은 **HS256 으로 alg 고정** — 같은 키의 HS384/512 서명·헤더 alg 조작은
|
|
492
|
+
통하지 않는다. **알려진 한계**: 토큰은 stateless 라 서버측 폐기(로그아웃·강제
|
|
493
|
+
무효화) 수단이 없다 — 유출된 리프레시 토큰은 만료(기본 7d)까지 유효하므로
|
|
494
|
+
민감한 앱은 `refreshTtl` 을 짧게 잡는다(서버측 폐기 목록은 v1 범위 밖).
|
|
495
|
+
|
|
496
|
+
- **JWT 스캐폴드 = `gaon g auth --jwt --app <api>`** (결정 338 · `--app` 필수 ·
|
|
497
|
+
web 불가 — web 은 세션이 정본). 페이지·회원가입 없이 스키마/모델(공유) +
|
|
498
|
+
토큰 컨트롤러(JSON 전용: `POST /session` 발급 · `POST /session/refresh` 재발급 ·
|
|
499
|
+
`GET /session` 현재 사용자[Bearer]) + `auth: { strategy:'jwt', secret, loadUser }`
|
|
500
|
+
배선 + `.env` 에 `<APP>_JWT_SECRET` 시드를 깐다. 계정은 web 앱 가입 또는
|
|
501
|
+
seed 로 만든다(API 앱에 공개 가입 없음).
|
|
502
|
+
|
|
492
503
|
- **API 앱은 프론트엔드가 없다(JSON 전용).** `apps/<app>/app.config.ts`(`auth: { strategy:'jwt', … }`)
|
|
493
504
|
+ `routes.ts` + `controllers/` 만 두면 된다 — `index.html`·`main.ts`·`pages/` 는 만들지 않는다.
|
|
494
505
|
앱 발견은 `routes.ts` 기준이라 `gaon serve` 가 이 앱을 `/<app>` 프리픽스로 정상 마운트하고,
|
|
@@ -568,6 +579,8 @@ export default controller({
|
|
|
568
579
|
- **액션이 아무것도 반환하지 않으면 204 No Content 다** — `this.render(...)`
|
|
569
580
|
를 호출만 하고 `return` 을 빼먹으면 컴파일은 통과하고 페이지가 조용히
|
|
570
581
|
빈 204 로 나간다. 렌더·리다이렉트·JSON 은 항상 `return` 과 함께 쓴다.
|
|
582
|
+
`gaon doctor` 의 **render-return** 검사가 이 패턴(호출만 하고 return 누락)을
|
|
583
|
+
경고로 잡는다(결정 340 · 의도된 204 는 응답 호출 없이 그냥 return).
|
|
571
584
|
- **라우트 타깃 형식 불량은 부팅 에러다(결정 293)** — `r.get('/x', 'posts')`
|
|
572
585
|
처럼 `#액션` 을 빠뜨리면 이전엔 조용히 라우트가 사라져 무신호 404 였다.
|
|
573
586
|
이제 `routes()` 가 부팅에서 throw 한다(`'<컨트롤러>#<액션>'` 형식 필수).
|
|
@@ -607,7 +620,8 @@ export default controller({
|
|
|
607
620
|
| 결정 150 | 앱 전역 공유 키 확장 — `app.config` sharedProps → 모든 렌더 자동 주입 · 코어 3종 예약(덮으면 throw) · 선언 병합 타입 · hidden 미유출 · useShared 로 읽기(§4.2) |
|
|
608
621
|
| 결정 119 | 목록 액션 페이지네이션 = `chain.paginate(page, perPage)` 종단(§4.3 · `agents/data.md`) · 손 조립 반정본 · result 통째로 render props 안전 |
|
|
609
622
|
| 결정 120 | 클라이언트 IP = `this.request.ip`(별도 표면 없음) · `web.clientIp` direct/proxy/header 로 rate limit·로깅과 같은 산출 배선(§4.4 · `agents/security.md`) |
|
|
610
|
-
| 결정 133 | 멀티파트 업로드(`this.file()`) CSRF
|
|
623
|
+
| 결정 133 | 멀티파트 업로드(`this.file()`) CSRF 검사는 `x-csrf-token` 헤더로만 — 바디 `_csrf` 는 스트리밍 파싱이라 검사 시점에 없다(§3 · 헤더 부재 시 403 + 수리 안내 · 부착은 결정 342 로 자동화) |
|
|
624
|
+
| 결정 342 | CSRF 부착 The One Way — `useForm`/`router` 상태 변경 visit 에 프레임웍이 `X-CSRF-Token` 자동 부착(라이브 페이지 props 출처 · 결정 341 · api() 와 단일 출처) · 수동 `_csrf` 바디/헤더 보일러플레이트 제거(스캐폴드 동기) · 명시 헤더는 존중(탈출구) |
|
|
611
625
|
| 결정 64 | 폼 API 는 `gaonjs/vue` 의 `useForm`·`router` 뿐 — 로그아웃 등 DELETE 는 `router.delete()`(`@inertiajs/vue3` 직접 import 금지 · `Inertia.post()` 유령 API 아님) |
|
|
612
626
|
| 결정 122 | 관계·hidden 값이 render 경계 `serializeProps` 를 넘어 새지 않는다 — hidden 컬럼 제외 유지(§4.2) |
|
|
613
627
|
| 결정 165 | 세션/CSRF 실패·415(지원 안 되는 Content-Type)를 코어가 Inertia-네이티브(409 풀 리로드+flash / 415 수리 안내)로 마감 — raw JSON 403 으로 앱을 깨지 않는다(§4.1 · 지원 타입 `application/json`·`multipart/form-data`) |
|
|
@@ -620,6 +634,10 @@ export default controller({
|
|
|
620
634
|
| 결정 296 | 세션 없는 앱의 `this.auth.login`/`logout` = fail-loud throw(조용한 no-op 금지 · §6) |
|
|
621
635
|
| 결정 297 | 초기 HTML 문서에도 `Vary: X-Inertia` + `Cache-Control: private, no-cache` — CDN/공유 캐시가 사용자별 data-page(csrf·currentUser)를 캐시하지 못하게 |
|
|
622
636
|
| 결정 298 | render props 순환 참조 = 스택 오버플로 대신 수리 안내 에러(같은 객체의 형제 중복(DAG)은 정상) |
|
|
637
|
+
| 결정 337 | JWT 하드닝 — secret 32자 하한(부팅) · 운영 dev 폴백 거부 · 검증 alg HS256 고정 · stateless 폐기 불가 한계 명기(§6) |
|
|
638
|
+
| 결정 338 | `gaon g auth --jwt --app <api>` — API 앱 토큰 스캐폴드(발급/재발급/내 정보 · 페이지·가입 없음 · `<APP>_JWT_SECRET` 시드 · §6) |
|
|
639
|
+
| 결정 339 | 앱 스코프 보안 override — `app.config` `security: { cors, rateLimit }`(생략 = 전역 상속 · rate limit 버킷은 앱 단위 · `agents/security.md` §1) |
|
|
640
|
+
| 결정 340 | doctor `render-return` — 응답 호출만 하고 return 누락 = 무신호 204 경고(함정 §알려진 함정) |
|
|
623
641
|
| E-1 | 파사드 = `gaonjs` · CLI = `gaon` |
|
|
624
642
|
|
|
625
643
|
## `@gaonjs/seal` 켠 앱
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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/
|
|
35
|
+
"@gaonjs/async": "0.17.0",
|
|
36
|
+
"@gaonjs/config": "0.22.0",
|
|
36
37
|
"@gaonjs/core": "0.2.4",
|
|
37
|
-
"@gaonjs/
|
|
38
|
+
"@gaonjs/data": "0.24.0",
|
|
38
39
|
"@gaonjs/i18n": "0.2.4",
|
|
39
|
-
"@gaonjs/mail": "0.
|
|
40
|
-
"@gaonjs/
|
|
41
|
-
"@gaonjs/web": "0.24.1"
|
|
40
|
+
"@gaonjs/mail": "0.4.0",
|
|
41
|
+
"@gaonjs/web": "0.27.0"
|
|
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})\""
|