@gaonjs/cli 0.31.0 → 0.31.1

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.
@@ -10,6 +10,14 @@ REDIS_URL=redis://127.0.0.1:6379
10
10
  # NATS — 실시간·비동기 백본(§7).
11
11
  NATS_URL=nats://127.0.0.1:4222
12
12
 
13
+ # 파일 스토리지(§7 · M8) — docker-compose.yaml 의 minio 서비스와 정합.
14
+ # STORAGE_BUCKET 은 compose 의 createbuckets 가 만드는 dev 버킷명과 같아야 한다.
15
+ # 운영은 R2/S3 endpoint·자격증명으로 교체하고, 버킷은 인프라에서 사전 생성한다.
16
+ STORAGE_ENDPOINT=http://127.0.0.1:9000
17
+ STORAGE_BUCKET={{PROJECT_NAME}}
18
+ STORAGE_ACCESS_KEY={{PROJECT_NAME}}
19
+ STORAGE_SECRET_KEY={{PROJECT_NAME}}_secret
20
+
13
21
  # 세션 · 쿠키 서명 비밀 (32자 이상, 운영은 반드시 교체).
14
22
  SESSION_SECRET=change-me-to-a-32-char-random-secret!!
15
23
  COOKIE_SECRET=change-me-too-32-char-random-secret!!
@@ -32,6 +32,13 @@
32
32
  realtime 허용). 끄거나 조정은 `createApp({ security: { securityHeaders: … } })`
33
33
  — `false` 로 전부 끔, `{ contentSecurityPolicy: '…' | false, hsts: false }` 로 조정.
34
34
  `helmet` 등 라이브러리를 따로 깔지 말 것(코어 내장 · 라이브러리 미의존).
35
+ - **스토리지 오리진 CSP 자동 배선 (결정 131)** — `gaon.config.ts` 의 storage(S3/R2/MinIO)
36
+ 설정이 있으면 코어가 그 오리진을 CSP 의 **img-src**(스토리지 이미지 `<img>` 표시)·
37
+ **connect-src**(브라우저 직접 presigned 업로드/다운로드)에 자동으로 더한다(seal→
38
+ `wasm-unsafe-eval`(결정 124)과 동형). 그래서 스토리지 이미지·직접 업로드를 쓰겠다고
39
+ **CSP 를 손으로 넓히거나 `contentSecurityPolicy: false` 로 끄지 말 것** — 오리진은
40
+ storage 설정에서 파생돼 자동으로 허용된다. 오리진은 `endpoint`·`publicUrl` 의
41
+ scheme+host(+port)만 뽑는다(비절대 URL 이면 부팅 에러 + 수리 안내).
35
42
 
36
43
  ### 2. 세션·CSRF·JWT
37
44
 
@@ -152,6 +152,34 @@ export default controller({
152
152
  ①이 검증까지 주므로 **모델이 있으면 ①을 먼저 고른다**. ②는 로그인 폼처럼
153
153
  전용 테이블이 없는 입력의 탈출구다.
154
154
 
155
+ **멀티파트 업로드 + CSRF — 토큰은 `x-csrf-token` 헤더로만 (결정 133):**
156
+
157
+ `this.file()` 업로드(멀티파트)의 CSRF 토큰은 **`x-csrf-token` 헤더**로 보낸다.
158
+ 멀티파트는 `parts()` 스트리밍이라 CSRF 검사(preHandler) 시점에 **바디가 아직
159
+ 파싱되지 않아** 폼 필드 `_csrf` 가 검사에 잡히지 않는다(구조적 한계 · 디스패처가
160
+ handler 안에서 파싱). 일반 폼(JSON/urlencoded)의 `_csrf` 바디 폴백은 멀티파트엔
161
+ 통하지 않는다. 파일이 있으면 `useForm` 이 자동으로 multipart 로 보내므로, 업로드
162
+ 제출은 **반드시 헤더**로 토큰을 실어야 한다.
163
+
164
+ ```vue
165
+ <script setup lang="ts">
166
+ import { useForm, useShared } from 'gaonjs/vue'
167
+ const shared = useShared() // csrf 는 자동 주입 공유 prop (결정 116)
168
+ const form = useForm({ avatar: null as File | null })
169
+
170
+ function submit() {
171
+ // 파일이 있으면 multipart — csrf 는 x-csrf-token 헤더로(바디 _csrf 는 안 걸림).
172
+ form.post('/uploads', { headers: { 'x-csrf-token': shared.csrf } })
173
+ }
174
+ </script>
175
+ ```
176
+
177
+ - **알려진 함정:** 업로드 폼을 일반 폼처럼 `useForm({ avatar, _csrf: shared.csrf })`
178
+ 로 짜면 `_csrf` 가 멀티파트 필드로 들어가 **검사 시점에 없어 403** 이 난다. 서버는
179
+ 이 경우 "→ x-csrf-token 헤더로 보내라" 는 수리 안내와 함께 403 을 돌려준다.
180
+ - 비멀티파트 폼은 지금처럼 `_csrf: shared.csrf` 바디 필드로 그대로 보낸다(§4 로그인
181
+ 예시). 멀티파트일 때만 헤더가 유일 경로다.
182
+
155
183
  ### 4. 데이터 경로 판단 — 루트 판단표가 정본
156
184
 
157
185
  데이터가 필요할 때는 루트 `AGENTS.md` 의 데이터 경로 4종 판단표를 따른다
@@ -440,6 +468,7 @@ export default controller({
440
468
  | 결정 117 | render props 에 예약 공유 키 = 컴파일 에러 + 런타임 방어(자동 주입값 조용한 덮어쓰기 금지 · §4.2) |
441
469
  | 결정 119 | 목록 액션 페이지네이션 = `chain.paginate(page, perPage)` 종단(§4.3 · `agents/data.md`) · 손 조립 반정본 · result 통째로 render props 안전 |
442
470
  | 결정 120 | 클라이언트 IP = `this.request.ip`(별도 표면 없음) · `web.clientIp` direct/proxy/header 로 rate limit·로깅과 같은 산출 배선(§4.4 · `agents/security.md`) |
471
+ | 결정 133 | 멀티파트 업로드(`this.file()`) CSRF 는 `x-csrf-token` 헤더로만 — 바디 `_csrf` 는 스트리밍 파싱이라 검사 시점에 없다(§3 · 헤더 부재 시 403 + 수리 안내) |
443
472
  | E-1 | 파사드 = `gaonjs` · CLI = `gaon` |
444
473
 
445
474
  ## `@gaonjs/seal` 켠 앱
@@ -77,3 +77,21 @@ services:
77
77
  interval: 2s
78
78
  timeout: 3s
79
79
  retries: 30
80
+
81
+ # 개발 버킷 생성 — this.file() → Storage.put() 이 첫 업로드부터 돌게 한다.
82
+ # MinIO 서버만 띄우면 버킷이 없어 첫 업로드가 NoSuchBucket 으로 실패한다 —
83
+ # minio 헬스 후 내장 mc 로 dev 버킷(.env 의 STORAGE_BUCKET = 기본 {{PROJECT_NAME}})을
84
+ # 한 번 만들고 종료한다(`--ignore-existing` 라 재기동에 멱등). minio 서버 이미지에
85
+ # mc 가 이미 들어 있어 별도 이미지(minio/mc)를 받지 않는다(첫 gaon dev 가 더 빠름).
86
+ # 운영(R2/S3)은 인프라에서 버킷을 사전 생성한다(앱 밖 관심사) — 이 컨테이너는 dev 전용.
87
+ createbuckets:
88
+ image: minio/minio:latest
89
+ depends_on:
90
+ minio:
91
+ condition: service_healthy
92
+ entrypoint: >
93
+ /bin/sh -c "
94
+ mc alias set local http://minio:9000 {{PROJECT_NAME}} {{PROJECT_NAME}}_secret &&
95
+ mc mb --ignore-existing local/{{PROJECT_NAME}}
96
+ "
97
+ restart: "no"
@@ -19,6 +19,23 @@ export default defineConfig({
19
19
  // NATS — 실시간·비동기 백본(§7). broadcast 전용(errata E-2).
20
20
  nats: process.env.NATS_URL ? { url: process.env.NATS_URL } : undefined,
21
21
 
22
+ // 파일 스토리지(§7 · M8). S3 호환(dev = MinIO · 운영 = R2/S3) — endpoint 만 바꾸면
23
+ // 같은 코드가 돈다. dev 버킷은 compose 의 createbuckets 가 만든다(첫 업로드부터 동작).
24
+ storage: process.env.STORAGE_ENDPOINT
25
+ ? {
26
+ default: 'main',
27
+ disks: {
28
+ main: {
29
+ driver: 's3',
30
+ bucket: process.env.STORAGE_BUCKET ?? '{{PROJECT_NAME}}',
31
+ endpoint: process.env.STORAGE_ENDPOINT,
32
+ accessKeyId: process.env.STORAGE_ACCESS_KEY,
33
+ secretAccessKey: process.env.STORAGE_SECRET_KEY,
34
+ },
35
+ },
36
+ }
37
+ : undefined,
38
+
22
39
  // 웹 서버 리슨 옵션. --port · env PORT 로 덮을 수 있다.
23
40
  web: {
24
41
  port: process.env.PORT ? Number(process.env.PORT) : 3000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.31.0",
3
+ "version": "0.31.1",
4
4
  "description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,11 +28,11 @@
28
28
  "typescript": "^5.9.0",
29
29
  "vite": "^7.0.0",
30
30
  "@gaonjs/async": "0.8.0",
31
- "@gaonjs/config": "0.10.0",
32
- "@gaonjs/data": "0.13.1",
33
- "@gaonjs/core": "0.2.1",
31
+ "@gaonjs/config": "0.11.0",
32
+ "@gaonjs/web": "0.14.0",
34
33
  "@gaonjs/mail": "0.1.3",
35
- "@gaonjs/web": "0.13.1"
34
+ "@gaonjs/data": "0.13.1",
35
+ "@gaonjs/core": "0.2.1"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""