@gaonjs/cli 0.27.1 → 0.27.2
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.
|
@@ -138,6 +138,11 @@ import 하면 순환 참조가 생기므로, 실제 연결은 부팅 시 프레
|
|
|
138
138
|
**타입 레벨로** 제외 (모든 타입에 확장). 페이지 props 에서 접근하면
|
|
139
139
|
컴파일 에러 — 유출이 타입 시스템에서 막힌다 (v0.15 §4.2 line
|
|
140
140
|
396–399 원문). 비밀번호 다이제스트가 대표: `passwordDigest: t.string().hidden()`.
|
|
141
|
+
런타임도 `serializeProps` 가 hidden 컬럼을 값에서 떨군다 —
|
|
142
|
+
**관계(`include`/지연) 로 로드한 대상 행에서도** 대상 테이블의 hidden 이
|
|
143
|
+
제외된다(결정 122). 즉 `Post.include('author')` 로 실은 `author`(User)를
|
|
144
|
+
render props 로 통째로 넘겨도 `passwordDigest` 는 나가지 않는다. hidden 값은
|
|
145
|
+
서버 코드에서는 그대로 읽힌다(직렬화에서만 제외 · §4.2).
|
|
141
146
|
- `.unique()` — 컬럼 레벨 UNIQUE 제약 (E-4).
|
|
142
147
|
- `.index()` — 컬럼 레벨 인덱스 (E-4).
|
|
143
148
|
- `.check(expr)` — 컬럼 레벨 CHECK 제약 (E-4).
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
`agents/web.md` §5)로만 다루고, 다이제스트는 hidden 컬럼
|
|
47
47
|
(`passwordDigest: t.string().hidden()`)에 저장한다 — 응답 경계에서
|
|
48
48
|
타입·런타임 양쪽으로 페이지 노출이 막힌다.
|
|
49
|
+
- **hidden 계약은 render props 로 나가는 모든 경로에 적용된다** — 직접
|
|
50
|
+
모델 행뿐 아니라 `include()`/지연 관계로 로드한 **대상 행**에서도 그
|
|
51
|
+
테이블의 hidden 이 제외된다(결정 122). 불변식: *페이지로 나가는 값은
|
|
52
|
+
`serializeProps` 를 통과하고, 그 결과 어디에도 hidden 컬럼명이 없다.*
|
|
53
|
+
민감 컬럼은 반드시 `.hidden()` 로 선언해야 이 보호가 작동한다.
|
|
49
54
|
|
|
50
55
|
### 4. 입력 안전 — `this.params` 고정 우선순위 (errata E-3 §5)
|
|
51
56
|
|
|
@@ -122,6 +127,10 @@ const rows = await Post.query()
|
|
|
122
127
|
`{ proxy }`·`{ header }` 를 선언한다(§6).
|
|
123
128
|
- **IP 로 인가 판단 금지** (결정 120) — "특정 IP = 관리자" 류는 위조·회전에 무너진다.
|
|
124
129
|
IP 는 rate limit·로깅까지만, 인가는 신원(세션/JWT)으로.
|
|
130
|
+
- **관계 행을 render props 로 넘길 때 hidden 을 잊지 않는다** (결정 122) —
|
|
131
|
+
hidden 보호는 `serializeProps` 통과 시 자동 적용되고 관계(`include`/지연)
|
|
132
|
+
행에도 적용된다. 단, 민감 컬럼을 `.hidden()` 로 **선언하지 않으면** 어느
|
|
133
|
+
경로로도 막히지 않는다 — 토큰·다이제스트·개인정보는 선언 시점에 hidden.
|
|
125
134
|
|
|
126
135
|
## 관련 결정 번호
|
|
127
136
|
|
|
@@ -132,3 +141,4 @@ const rows = await Post.query()
|
|
|
132
141
|
| §7 (v0.15) | 세션 앱별 분리 · JWT 는 API 앱 전용 |
|
|
133
142
|
| 결정 93 (W2) | 기본 web 앱 세션 기본 배선 = CSRF 기본 켬 실태 · doctor `csrf-wiring` 경고 |
|
|
134
143
|
| 결정 120 | 클라이언트 IP 신뢰 = `web.clientIp` direct/proxy/header · 헤더는 신뢰 홉 전제에서만 · IP 는 약한 신호(인가 금지) · `this.request.ip` 단일 산출(§6 · `agents/web.md` §4.4) |
|
|
144
|
+
| 결정 122 | hidden 계약은 관계(`include`/지연) 행에도 적용 — render props 로 나가는 모든 값은 `serializeProps` 통과 후 hidden 컬럼명 부재 |
|
|
@@ -123,6 +123,41 @@ BEGIN/COMMIT 을 여는 대상이라, 테스트를 바깥 트랜잭션으로 감
|
|
|
123
123
|
그래서 격리는 service 가 실제로 커밋하는 운영 경로를 그대로 두고 매 테스트
|
|
124
124
|
뒤 truncate 로 비운다 — service 든 아니든 항상 안전하다.
|
|
125
125
|
|
|
126
|
+
### 6. 직렬화 경계 테스트 — 관계 경유 hidden 을 반드시 포함한다 (결정 122)
|
|
127
|
+
|
|
128
|
+
`.hidden()` 컬럼(예: `passwordDigest`)은 페이지 props 로 나가면 안 된다(§4.2).
|
|
129
|
+
이걸 검증하는 테스트는 **직접 모델 행만 보면 안 된다** — `include()`/지연 관계
|
|
130
|
+
접근자로 로드한 **관계 행**도 함께 렌더해 hidden 이 떨어지는지 봐야 한다.
|
|
131
|
+
관계 경유가 v1.4.1 에서 실제로 샜던 지점이고(결정 122), 직접 행만 보던
|
|
132
|
+
기존 테스트는 그 유출을 못 잡았다.
|
|
133
|
+
|
|
134
|
+
정본 = "직렬화 결과 어디에도 hidden 컬럼명이 없다" 를 **재귀로** 단언한다
|
|
135
|
+
(배열·중첩 관계 전부 훑는 헬퍼). 관계를 포함한 픽스처에 적용한다:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// 직렬화 결과(JSON-safe)에 hidden 컬럼명이 어느 깊이에도 없음을 확인.
|
|
139
|
+
function assertNoHiddenLeak(value: unknown, hidden: readonly string[]): void {
|
|
140
|
+
const walk = (v: unknown): void => {
|
|
141
|
+
if (v === null || typeof v !== 'object') return
|
|
142
|
+
if (Array.isArray(v)) return void v.forEach(walk)
|
|
143
|
+
for (const [k, child] of Object.entries(v)) {
|
|
144
|
+
if (hidden.includes(k)) throw new Error(`hidden 유출: ${k}`)
|
|
145
|
+
walk(child)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
walk(value)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 관계를 포함해 렌더 — belongsTo·hasMany·hasOne·belongsToMany 를 커버.
|
|
152
|
+
const post = await Post.include('author').first() // author = User(passwordDigest hidden)
|
|
153
|
+
const props = serializeProps({ post })
|
|
154
|
+
assertNoHiddenLeak(props, ['passwordDigest']) // 관계 author 까지 재귀 확인
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
hidden 값은 **서버 코드에서는 여전히 읽힌다**(직렬화 경계에서만 제외 · §4.2) —
|
|
158
|
+
`serializeProps` 전에는 `user.passwordDigest` 가 정상 접근됨을 함께 단언해 계약을
|
|
159
|
+
양쪽으로 고정한다.
|
|
160
|
+
|
|
126
161
|
## 정본 예시
|
|
127
162
|
|
|
128
163
|
위 §4 의 `welcomeMail.integration.test.ts` 가 잡 검증의 정본 예시이고,
|
|
@@ -139,12 +174,16 @@ BEGIN/COMMIT 을 여는 대상이라, 테스트를 바깥 트랜잭션으로 감
|
|
|
139
174
|
소비한다. 큐·스트림 프리픽스를 테스트 전용으로.
|
|
140
175
|
- **워커 정리** — 직접 `runWorker` 를 쓰면 테스트 종료 전 `stop()` 을
|
|
141
176
|
보장하라 (`expectJobProcessed` 는 자동 정리).
|
|
177
|
+
- **직렬화 테스트가 직접 행만 봄** — hidden 검증을 `Model.create()` 결과
|
|
178
|
+
한 행에만 하면 관계 경유 유출(결정 122)을 못 잡는다. `include()`/지연
|
|
179
|
+
관계 행까지 렌더해 재귀로 확인한다(§6).
|
|
142
180
|
|
|
143
181
|
## 관련 결정 번호
|
|
144
182
|
|
|
145
183
|
| 결정 | 내용 |
|
|
146
184
|
|---|---|
|
|
147
185
|
| 결정 42 | 비동기 테스트 헬퍼 `expectJobProcessed` (`gaonjs/testing`) |
|
|
186
|
+
| 결정 122 | 직렬화 경계 테스트는 관계 경유 hidden 을 반드시 포함(재귀 no-leak 단언) |
|
|
148
187
|
| 결정 111 | `gaon test` 테스트 DB 자동 준비 + `connectTestDatabase`·`truncateAll` 격리(truncate · service COMMIT 실측) |
|
|
149
188
|
| §9 (v0.15) | 실 인프라 필수 · 목업/인메모리 금지 |
|
|
150
189
|
| 결정 32 | 잡 발행 위치 자유 — publish 함수가 서비스 경유여도 검증 대상 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"typescript": "^5.9.0",
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
|
-
"@gaonjs/
|
|
31
|
-
"@gaonjs/data": "0.13.
|
|
30
|
+
"@gaonjs/config": "0.8.1",
|
|
31
|
+
"@gaonjs/data": "0.13.1",
|
|
32
32
|
"@gaonjs/core": "0.2.1",
|
|
33
|
-
"@gaonjs/
|
|
34
|
-
"@gaonjs/
|
|
35
|
-
"@gaonjs/
|
|
33
|
+
"@gaonjs/async": "0.6.1",
|
|
34
|
+
"@gaonjs/web": "0.10.0",
|
|
35
|
+
"@gaonjs/mail": "0.1.3"
|
|
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})\""
|