@gaonjs/cli 0.41.4 → 0.41.5
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/templates/project/AGENTS.md.tpl +2 -2
- package/dist/templates/project/agents/async.md.tpl +12 -7
- package/dist/templates/project/agents/data.md.tpl +15 -15
- package/dist/templates/project/agents/frontend.md.tpl +1 -1
- package/dist/templates/project/agents/seal.md.tpl +3 -1
- package/dist/templates/project/agents/security.md.tpl +3 -3
- package/dist/templates/project/agents/testing.md.tpl +4 -1
- package/dist/templates/project/agents/web.md.tpl +2 -2
- package/package.json +4 -4
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
이 문서는 **AI 코딩 에이전트**(Claude · Codex · Cursor · Copilot 등)와
|
|
4
4
|
사람 개발자가 Gaon 프로젝트에서 작업할 때 참조하는 관례의 진입점이다.
|
|
5
5
|
정본은 설계 문서(`docs/gaondesignv0.17.md` · v1.0 출시 기준 스냅샷 ·
|
|
6
|
-
v0.15+errata→v0.16→v0.17 · 결정 31~
|
|
6
|
+
v0.15+errata→v0.16→v0.17 · 결정 31~91)이며, 관례 문서는 **2층 구조**다 (결정 40):
|
|
7
7
|
|
|
8
8
|
- **이 파일 (코어)** — 절대 규칙 · 로직 배치 판단표 · 검증 루프 ·
|
|
9
9
|
카테고리 색인. 여기엔 요약만 있다.
|
|
@@ -264,7 +264,7 @@ gaon doctor # 정적 검사 27종 (§2.2)
|
|
|
264
264
|
|
|
265
265
|
## 7. 참고 문서
|
|
266
266
|
|
|
267
|
-
- 설계 정본: `docs/gaondesignv0.17.md` (v1.0 출시 기준 스냅샷 · 결정 31~
|
|
267
|
+
- 설계 정본: `docs/gaondesignv0.17.md` (v1.0 출시 기준 스냅샷 · 결정 31~91) ·
|
|
268
268
|
이력 동결 = `gaondesignv0.16.md`·`v0.15.md` + errata E-1~E-5
|
|
269
269
|
(E-1 파사드명 · E-2 실시간 TCP · E-3 JSON 액션/params · E-4 컬럼·
|
|
270
270
|
체이닝 · E-5 컴포저블·레이아웃).
|
|
@@ -55,7 +55,7 @@ export default schedule((s) => {
|
|
|
55
55
|
|
|
56
56
|
## 정본 규칙
|
|
57
57
|
|
|
58
|
-
### 1. 잡 (`job()`) (`packages/async/src/jobs.ts
|
|
58
|
+
### 1. 잡 (`job()`) (`packages/async/src/jobs.ts`)
|
|
59
59
|
|
|
60
60
|
잡은 도메인 소속이다 — `domain/jobs/*.ts` 에 파일을 놓으면 등록이고,
|
|
61
61
|
어느 앱에서 큐잉하든 같은 워커(`gaon work`)가 처리한다. 모델·서비스와
|
|
@@ -240,6 +240,8 @@ export default schedule((s) => {
|
|
|
240
240
|
- **소비 = 워커 전체 분산** — 발행된 잡은 NATS JetStream 큐 그룹으로 **모든**
|
|
241
241
|
워커에 로드밸런싱된다. 발행은 1인, 처리는 N인.
|
|
242
242
|
- **exactly-once(발행 기준)** — 한 스케줄 틱은 리더 1인이 한 번만 발행한다.
|
|
243
|
+
리더 교체(페일오버) 순간 구·신 리더가 같은 틱을 겹쳐 발행해도, 결정론적
|
|
244
|
+
dedupe 키 + JetStream 중복 윈도우가 이를 1회로 수렴시킨다(결정 233).
|
|
243
245
|
잡 자체는 재시도(백오프)가 있으니 **핸들러는 멱등**하게 짠다(같은 잡이 두 번
|
|
244
246
|
처리돼도 안전하게).
|
|
245
247
|
- **`gaon serve` 는 스케줄러를 돌리지 않는다** — 스케줄·리더 선출·아웃박스
|
|
@@ -248,7 +250,7 @@ export default schedule((s) => {
|
|
|
248
250
|
원인은 `gaon work` 를 안 띄운 것이다(개발은 `gaon dev` 가 work 를 자동 기동
|
|
249
251
|
하므로 해당 없음 · 결정 211 · §6).
|
|
250
252
|
|
|
251
|
-
#### 시간대 (결정 202)
|
|
253
|
+
#### 시간대 (결정 202·230)
|
|
252
254
|
|
|
253
255
|
`s.daily.at('04:00', Job)`·`s.cron('0 9 * * 1', Job)` 는 **서버 로컬 타임존**의
|
|
254
256
|
wall-clock 으로 매치한다(`new Date()` 로컬 시·분·요일). v1 은 **잡별 타임존
|
|
@@ -320,8 +322,9 @@ drain — 스케줄러 리더를 반납하고 진행 중인 잡을 완료한 뒤
|
|
|
320
322
|
따로 놀아 무의미하다(결정 88 ①). 그래서 백엔드는 Redis 다: 설정에 `redis`
|
|
321
323
|
가 있으면 **`gaon serve` 와 `gaon work` 둘 다** 분산 락을 자동 배선한다(결정
|
|
322
324
|
202 · wireDomain 공통 경로) — 그래서 **잡·서비스 안에서도 `lock()` 을 쓸 수
|
|
323
|
-
있다**(예: 중첩 방지 · §5).
|
|
324
|
-
뮤텍스로 조용히 떨어지지 않고 수리 안내와 함께
|
|
325
|
+
있다**(예: 중첩 방지 · §5). **운영(`NODE_ENV=production`)에서 `redis` 미설정
|
|
326
|
+
상태로 `lock()` 을 부르면 로컬 뮤텍스로 조용히 떨어지지 않고 수리 안내와 함께
|
|
327
|
+
throw** 한다(개발·테스트는 in-memory 백엔드로 폴백해 단일 프로세스에서 그대로 돈다).
|
|
325
328
|
|
|
326
329
|
```ts
|
|
327
330
|
import { lock } from 'gaonjs/async'
|
|
@@ -392,8 +395,8 @@ async create() {
|
|
|
392
395
|
(`agents/testing.md`).
|
|
393
396
|
- **동시 실행 방지에 로컬 뮤텍스·플래그 금지** (결정 147) — `let running = false`
|
|
394
397
|
같은 프로세스 로컬 가드는 멀티 인스턴스에서 안 먹는다. `lock(key, fn)` 을
|
|
395
|
-
쓴다. `redis` 미설정이면 `lock()` 이 수리 안내로 throw 하니 조용한
|
|
396
|
-
|
|
398
|
+
쓴다. 운영에서 `redis` 미설정이면 `lock()` 이 수리 안내로 throw 하니 조용한
|
|
399
|
+
파손이 없다(개발·테스트는 in-memory 폴백).
|
|
397
400
|
|
|
398
401
|
## 관련 결정 번호
|
|
399
402
|
|
|
@@ -406,6 +409,8 @@ async create() {
|
|
|
406
409
|
| 결정 42 | 비동기 테스트 헬퍼 `expectJobProcessed` (`agents/testing.md`) |
|
|
407
410
|
| 결정 200 | DLQ 재처리 `retryDlq(nats, id)` (이미 설정된 잡 전송 보존 · DLQ 재적재 후 원본 삭제) |
|
|
408
411
|
| 결정 201 | graceful drain 계약 (serve 인플라이트 요청 완결 · work 진행 잡 완결 · §6) |
|
|
409
|
-
| 결정 202 | 락·캐시 백엔드 serve·work 공통 배선(wireDomain) · 잡/서비스 lock() 가능 · 스케줄러
|
|
412
|
+
| 결정 202 | 락·캐시 백엔드 serve·work 공통 배선(wireDomain) · 잡/서비스 lock() 가능 · 스케줄러 중첩 방지(§5·§7) |
|
|
413
|
+
| 결정 230 | 스케줄러/앱 시간대 = `gaon.config.ts` 의 `timezone` → `process.env.TZ`(config 가 런치 `TZ` 보다 우선 · 앱 전역 단일 타임존 · §5) |
|
|
414
|
+
| 결정 233 | 크론 리더 페일오버 중복 발행 dedupe (결정론적 dedupe 키 + JetStream 중복 윈도우로 틱당 1회 발행 수렴 · §5) |
|
|
410
415
|
| 결정 211 | `gaon dev` all-in-one — serve·work·hub 자동 기동 · dev 워커 동시성 4(`GAON_WORKER_CONCURRENCY`) · `--no-work`/`--no-hub` (§6) |
|
|
411
416
|
| §7 | 비동기 배터리 원문 (백오프 기본값 = M7 벤치마크 확정) |
|
|
@@ -100,7 +100,7 @@ import 하면 순환 참조가 생기므로, 실제 연결은 부팅 시 프레
|
|
|
100
100
|
이지 `created_at` 아님). 파일명 casing 은 doctor `schema-filename` 이
|
|
101
101
|
강제한다(단수/복수는 대상 밖 · 결정 38).
|
|
102
102
|
|
|
103
|
-
### 2. 컬럼 타입 전체 (실 구현 · `packages/data/src/schema.ts
|
|
103
|
+
### 2. 컬럼 타입 전체 (실 구현 · `packages/data/src/schema.ts`)
|
|
104
104
|
|
|
105
105
|
| 빌더 | SQL 타입 | TS 타입 | 비고 |
|
|
106
106
|
|---|---|---|---|
|
|
@@ -162,14 +162,14 @@ export const posts = table('posts', {
|
|
|
162
162
|
})
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
### 4. 체이닝 전체 (`packages/data/src/model.ts
|
|
165
|
+
### 4. 체이닝 전체 (`packages/data/src/model.ts`)
|
|
166
166
|
|
|
167
167
|
체이닝 표면은 아래 표가 **전부**다. 표에 없는 메서드
|
|
168
168
|
(`destroy`·`findBy`·`paginate`·`order`·해시 인자
|
|
169
169
|
`where({...})` 같은 다른 ORM 관습)를 추측해서 쓰지 말 것 — 표
|
|
170
170
|
바깥의 쿼리는 §5 `Post.query()` 탈출구로 내려간다.
|
|
171
171
|
|
|
172
|
-
**where op 12종** (`model.ts
|
|
172
|
+
**where op 12종** (`model.ts`):
|
|
173
173
|
|
|
174
174
|
| 분류 | op | val 인자 |
|
|
175
175
|
|---|---|---|
|
|
@@ -177,7 +177,7 @@ export const posts = table('posts', {
|
|
|
177
177
|
| 컬렉션 (`WhereOpIn`) | `in` `not in` | `ReadonlyArray<Row[col]>` |
|
|
178
178
|
| Null (`WhereOpNull`) | `is null` `is not null` | **없음** — 2-인자 호출 `where('deletedAt', 'is null')` |
|
|
179
179
|
|
|
180
|
-
**체이닝 메서드 전체** (`Chain` · `model.ts
|
|
180
|
+
**체이닝 메서드 전체** (`Chain` · `model.ts`):
|
|
181
181
|
|
|
182
182
|
| 메서드 | 시그니처 | 반환 | 비고 |
|
|
183
183
|
|---|---|---|---|
|
|
@@ -193,7 +193,7 @@ export const posts = table('posts', {
|
|
|
193
193
|
| `first` | `()` | `Promise<Rec \| undefined>` | 자동 `limit 1` |
|
|
194
194
|
| `all` | `()` | `Promise<Rec[]>` | |
|
|
195
195
|
| `paginate` | `(page, perPage)` | `Promise<PaginatedResult<Rec>>` | **체인 종단 페이지네이션**(결정 119) — `{ rows, total, page, pageCount, perPage }` 를 한 번에. count·rows 를 내부 계산(호출자 쿼리 한 번). page/perPage **클램프 내장**(0·음수·초과 = 마지막 페이지 · total 0 = pageCount 1). total=**number**. **result 통째로 render props 안전**(rows 는 Serialized 경계). `latest()` 타이브레이커(결정 110)로 페이지 경계 결정적. 집계 그룹(GroupChain)엔 없음 |
|
|
196
|
-
| `count` | `()` | `Promise<bigint>` | driver 별 반환을 **bigint 로 통일** (E-4 (g) · `model.ts
|
|
196
|
+
| `count` | `()` | `Promise<bigint>` | driver 별 반환을 **bigint 로 통일** (E-4 (g) · `model.ts`) |
|
|
197
197
|
| `exists` | `()` | `Promise<boolean>` | |
|
|
198
198
|
| `sum` · `avg` | `(col)` | `Promise<string \| null>` | numeric 정확성 · 대상 행 없으면 null |
|
|
199
199
|
| `min` · `max` | `(col)` | `Promise<Row[col] \| null>` | 컬럼 타입 그대로 |
|
|
@@ -222,7 +222,7 @@ export const posts = table('posts', {
|
|
|
222
222
|
> 테이블 Rec 으로 좁혀 "정답이 하나" 원칙을 지킨다 — 조인 테이블 컬럼까지
|
|
223
223
|
> 필요한 SELECT 는 여전히 `Post.query()` 탈출구다.
|
|
224
224
|
|
|
225
|
-
**루트 전용** (`ModelApi` · `model.ts
|
|
225
|
+
**루트 전용** (`ModelApi` · `model.ts`) — 체인 중간에서는 못 쓴다:
|
|
226
226
|
|
|
227
227
|
| 메서드 | 시그니처 | 반환 | 비고 |
|
|
228
228
|
|---|---|---|---|
|
|
@@ -256,7 +256,7 @@ export const posts = table('posts', {
|
|
|
256
256
|
> 합계에서 **정밀도를 잃는**(lossy) breaking 이라 "어림 금지" 원칙에 어긋나 기각했다
|
|
257
257
|
> (결정 91). 쓰기 개수(number)와 읽기 집계(string/bigint)는 축이 다르다.
|
|
258
258
|
|
|
259
|
-
**레코드(`Rec`) 내장** (`model.ts
|
|
259
|
+
**레코드(`Rec`) 내장** (`model.ts`):
|
|
260
260
|
|
|
261
261
|
- `rec.update(patch)` — `Partial<Row>` 부분 갱신, 갱신된 Rec 반환.
|
|
262
262
|
- `rec.delete()` — id 기준 단건 삭제, `Promise<void>` (M2C). 벌크는
|
|
@@ -290,7 +290,7 @@ methods: {
|
|
|
290
290
|
}
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
**체인 상태 전이 주의** (`model.ts
|
|
293
|
+
**체인 상태 전이 주의** (`model.ts`):
|
|
294
294
|
|
|
295
295
|
- `select()`·`include()` 이후에도 빌더 메서드(`where`·`orWhere`·
|
|
296
296
|
`whereIn`·`orderBy`·`reorder`·`latest`·`limit`·`offset`)는 전부
|
|
@@ -314,7 +314,7 @@ methods: {
|
|
|
314
314
|
> "…내부 구현은 Kysely 위에 얹으므로, 복잡한 쿼리는 언제든
|
|
315
315
|
> `Post.query()`로 내려가 순수 쿼리 빌더를 쓸 수 있다(탈출구)."
|
|
316
316
|
|
|
317
|
-
실 구현 (E-4 (h) 정정 · `model.ts
|
|
317
|
+
실 구현 (E-4 (h) 정정 · `model.ts`):
|
|
318
318
|
|
|
319
319
|
```ts
|
|
320
320
|
// 조인·집계·CTE 등 복잡 쿼리
|
|
@@ -331,10 +331,10 @@ const rows = await Post.query()
|
|
|
331
331
|
|
|
332
332
|
두 타입은 이름이 비슷하지만 **별개**다 (E-4 §7).
|
|
333
333
|
|
|
334
|
-
- `packages/vue/src/serialize.ts
|
|
334
|
+
- `packages/vue/src/serialize.ts` — **`Serialized<T>`**: 임의 값의
|
|
335
335
|
JSON-safe 매핑 (Date → string, bigint → string, Hidden 브랜드 제외).
|
|
336
336
|
`api()` 반환 타입 · `pageProps<'app:ctrl#action'>()` 결과 타입이 이것.
|
|
337
|
-
- `packages/data/src/schema.ts
|
|
337
|
+
- `packages/data/src/schema.ts` — **`SerializedOf<Defs>`**: 테이블
|
|
338
338
|
스키마 defs 의 SerializedOf — hidden 컬럼 키 제외한 Row.
|
|
339
339
|
|
|
340
340
|
혼동 유발이라 이름 분리를 유지한다 (E-4 (i) 결정).
|
|
@@ -370,11 +370,11 @@ const rows = await Post.query()
|
|
|
370
370
|
안전하고, 재시도·보정은 잡/아웃박스로 다룬다.
|
|
371
371
|
|
|
372
372
|
```ts
|
|
373
|
-
// domain/services/
|
|
373
|
+
// domain/services/placeOrder.ts — main 커밋 성공 뒤에만 analytics 기록 (파일명 camelCase)
|
|
374
374
|
import { service, afterCommit } from 'gaonjs/service'
|
|
375
375
|
import { getConnection } from 'gaonjs/data'
|
|
376
376
|
|
|
377
|
-
export const
|
|
377
|
+
export const PlaceOrder = service(async (input: { userId: string; total: number }) => {
|
|
378
378
|
const order = await Order.create({ userId: input.userId, total: input.total }) // main 트랜잭션
|
|
379
379
|
|
|
380
380
|
// 커넥션을 가로지르는 쓰기는 트랜잭션 밖 — 커밋 성공 뒤에만.
|
|
@@ -394,7 +394,7 @@ export const placeOrder = service(async (input: { userId: string; total: number
|
|
|
394
394
|
- **왜 `afterCommit`** — main 이 롤백되면 analytics 기록도 일어나지 않아야 한다.
|
|
395
395
|
`afterCommit` 은 커밋이 성공한 경우에만 콜백을 돈다(§9 · `agents/async.md`).
|
|
396
396
|
|
|
397
|
-
### 8. 모델 정의 (`model()`) (`packages/data/src/model.ts
|
|
397
|
+
### 8. 모델 정의 (`model()`) (`packages/data/src/model.ts`)
|
|
398
398
|
|
|
399
399
|
`model()` 은 스키마(§1)를 Kysely 위의 실행 가능한 API 로 감싼다 —
|
|
400
400
|
scope·인스턴스 메서드를 함께 선언하고, 나머지 체이닝(§4)·CRUD 는
|
|
@@ -443,7 +443,7 @@ await post.publish() // 인스턴스 메서
|
|
|
443
443
|
- **두 번째 인자** = `{ scopes?, methods?, hooks? }`. `scopes` 값은
|
|
444
444
|
`(q) => q.where(...)` 형태로 쿼리를 좁히는 함수, `methods` 는 인스턴스
|
|
445
445
|
메서드(`this` 로 레코드 필드·관계에 접근), `hooks` 는 `beforeCreate`
|
|
446
|
-
하나 (`model.ts
|
|
446
|
+
하나 (`model.ts`).
|
|
447
447
|
- **스코프는 반드시 `scopes: {}` 객체 안에** 선언한다 (NAMESPACED) —
|
|
448
448
|
model() 밖 별도 함수·프로퍼티로 흉내내지 않는다.
|
|
449
449
|
- **파라미터 스코프** (M2C · 결정 31) — 첫 인자 `q` 는 고정, 그 뒤
|
|
@@ -95,7 +95,7 @@ async function search(q: string) {
|
|
|
95
95
|
|
|
96
96
|
- **시그니처** — `api(key, params?, opts?)`. 제네릭 타입 인자를 직접
|
|
97
97
|
붙이지 않는다 — `key` 값 자체가 `keyof GaonRouteMap` 으로 좁혀져
|
|
98
|
-
반환 타입을 결정한다 (`packages/vue/src/api.ts
|
|
98
|
+
반환 타입을 결정한다 (`packages/vue/src/api.ts`).
|
|
99
99
|
- **params** — 라우트에 `:id` 같은 자리표시자가 있으면 거기서 채우고,
|
|
100
100
|
남는 값은 GET 이면 쿼리스트링, 그 외 메서드는 JSON 본문으로 실린다
|
|
101
101
|
(서버 `this.params` 우선순위와 대칭 · `agents/web.md` §3).
|
|
@@ -183,4 +183,6 @@ seal 앱 응답에만 `script-src` 에 `'wasm-unsafe-eval'` 을 **자동 주입*
|
|
|
183
183
|
- **결정 124** — §3.1 개정: **app-side 정적 주입**(변수 동적 import 폐기) · **wasm 표면 은닉**(불투명 함수 · domain/ua/path 를 wasm 이 확보 · 미끼 시크릿 내장) · **WS 클라 봉인**(`setWsFrameCodec`) · **seal 앱 한정 CSP** · doctor `seal-security` main.ts 배선 검사 + `seal-client-wiring` fixer · 실 브라우저 e2e 게이트 · 부수 정정(`.wasm` MIME · `session.csrf` forwarding).
|
|
184
184
|
- **결정 125** — **Inertia 네비게이션 평문 P0** 수정: 봉인 대상 판별기(`isSealTarget`)에 `X-Inertia: true` 를 편입. Inertia GET 방문은 `Accept: text/html` 로 와 application/json 이 없어 자동 면제되던 탓에 응답 props 가 평문으로 새어나갔다(클라 인터셉터는 시그널을 붙였으나 서버가 봉인 안 함). 네비게이션 봉인 e2e 를 seal blocking 게이트에 편입(실 vite+chromium · wire 봉인/`?q=` 왕복 단언).
|
|
185
185
|
- **결정 222** — **클라 WS 수신 fail-open P1** 수정: 클라 `wsDecode`(client.ts)가 `P:` 평문·무prefix 프레임을 throw 없이 원문 통과시켜, 서버는 requireDecrypt 로 거부하는데 클라만 주입된 평문을 소비하던 봉인 파괴. `wsDecode` 를 서버 `SealWsTerminator` 와 대칭으로 만들어 `E:` 만 개봉·`P:`/무prefix 거부. `useChannel` 은 개봉 실패를 조용히 드롭하지 않고 소켓을 **4500 종료**(서버 대칭) + 콘솔 명시 + 재연결 안 함.
|
|
186
|
-
- **결정 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
|
|
186
|
+
- **결정 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 폴백 = 단일 인스턴스 전용")와 상충. 폴백+경고가 비파괴적·정본 정합.
|
|
187
|
+
- **결정 224** — **최초 문서 data-page 평문 유출 P0** 수정: 서버가 주입한 **진짜** data-page 만 `data-gaon-seal-target` sentinel 로 특정해 봉인하고, 봉인 후에도 평문 data-page 잔재가 남으면 fail-closed 로 throw(§2·§5). seal 풀스택/브라우저 e2e 를 blocking 배포 게이트에 편입.
|
|
188
|
+
- **결정 248** — seal/web **에러 핸들러 단일화**(FSTWRN004): seal 플러그인은 자기 `setErrorHandler` 를 등록하지 않고(`installErrorHandler:false`) web 스코프가 하나만 등록한다. **seal 배선 코드는 자체 에러 핸들러를 달지 말 것**(중복 등록 = FSTWRN004 · 아키텍처 경계 · §4).
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
`Referrer-Policy: strict-origin-when-cross-origin`·`Cross-Origin-Opener-Policy`
|
|
30
30
|
를 자동으로 붙인다. 기본 CSP 는 Inertia SPA + vite 스택에서 안 깨지게 튜닝돼
|
|
31
31
|
있다(`script-src 'self'`·인라인 스타일 허용·`connect-src ... ws: wss:` 로
|
|
32
|
-
realtime 허용). 끄거나 조정은 `
|
|
32
|
+
realtime 허용). 끄거나 조정은 `gaon.config.ts` 의 `web.security.securityHeaders` 로
|
|
33
33
|
— `false` 로 전부 끔, `{ contentSecurityPolicy: '…' | false, hsts: false }` 로 조정.
|
|
34
34
|
`helmet` 등 라이브러리를 따로 깔지 말 것(코어 내장 · 라이브러리 미의존).
|
|
35
35
|
- **스토리지 오리진 CSP 자동 배선 (결정 131)** — `gaon.config.ts` 의 storage(S3/R2/MinIO)
|
|
@@ -182,8 +182,8 @@
|
|
|
182
182
|
|
|
183
183
|
```ts
|
|
184
184
|
// 시크릿 — env 로만 (gaonjs/env)
|
|
185
|
-
import {
|
|
186
|
-
const apiKey =
|
|
185
|
+
import { env } from 'gaonjs/env'
|
|
186
|
+
const apiKey = env('PAYMENT_API_KEY') // .env / 배포 환경변수
|
|
187
187
|
|
|
188
188
|
// raw SQL 값은 바인딩 위치에만
|
|
189
189
|
import { sql } from 'kysely'
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- `gaon test` 가 테스트 전용 데이터베이스(`<db>_test`)를 **자동 준비**한다
|
|
22
22
|
— 없으면 만들고(CREATE DATABASE) 마이그레이션까지 적용한 뒤 vitest 를
|
|
23
23
|
돌린다. 스캐폴드 `test/setup.ts` 가 그 DB 에 붙고(`connectTestDatabase`)
|
|
24
|
-
매 테스트 뒤 전 테이블을 비운다(`
|
|
24
|
+
매 테스트 뒤 전 커넥션의 테이블을 비운다(`truncateAllConnections`). 아래 §5 참고.
|
|
25
25
|
- `gaon test` 가 잡·이벤트 NATS 스트림도 **자동 격리**한다(결정 130) —
|
|
26
26
|
테스트 프로세스에 `GAON_STREAM_PREFIX` 를 주입해 스트림·subject 가
|
|
27
27
|
`GAON_TEST_JOBS`·`test.gaon.jobs.>` 로 갈린다. 같은 접두가 **NATS KV 버킷**
|
|
@@ -282,6 +282,9 @@ hidden 값은 **서버 코드에서는 여전히 읽힌다**(직렬화 경계에
|
|
|
282
282
|
| 결정 215 (13차 W3) | `expectEventProcessed` 가 trigger 뒤 아웃박스 1회 자동 드레인 — `service()` 경유 emit(결정 144 스테이징)도 같은 헬퍼로 확증(§4.1) · 명시 탈출구 `drainOutbox()` |
|
|
283
283
|
| 결정 122 | 직렬화 경계 테스트는 관계 경유 hidden 을 반드시 포함(재귀 no-leak 단언) |
|
|
284
284
|
| 결정 111 | `gaon test` 테스트 DB 자동 준비 + `connectTestDatabase`·`truncateAll` 격리(truncate · service COMMIT 실측) |
|
|
285
|
+
| 결정 137 | `truncateAllConnections()` — 등록된 **모든** 커넥션 순회 격리(멀티 커넥션 §4.5 · 스캐폴드 `test/setup.ts` 기본) |
|
|
286
|
+
| 결정 136 | `connectTestDatabase` 가 스토리지·메일 하네스도 배선(버킷 `<bucket>-test`·MailPit) — 배터리 테스트가 실 인프라로 격리 |
|
|
287
|
+
| 결정 249 | `connectTestDatabase` 가 락·캐시 백엔드도 in-memory 로 배선 — `lock()`·`cache()` 쓰는 잡·서비스가 `gaon test` 에서도 그대로(이전 `LockBackendUnconfiguredError` 해소 · §5) |
|
|
285
288
|
| 결정 130 | `gaon test` 가 NATS 스트림도 자동 격리(`GAON_STREAM_PREFIX` → `GAON_TEST_JOBS`·`test.gaon.jobs.>`) — 개발 워커 병행 시 잡 누출 방지(규칙 10 이행) |
|
|
286
289
|
| 결정 203 | 같은 접두를 **NATS KV 버킷**에도 적용(`gaon_lease`·프레즌스·허브 → `test_gaon_lease` 등) — 스트림만 격리하던 결정 130 의 빈틈(KV 미격리)을 메움. 스케줄러 리스 경합·크로스-프리픽스 KV 누출 방지 |
|
|
287
290
|
| §9 (v0.15) | 실 인프라 필수 · 목업/인메모리 금지 |
|
|
@@ -24,7 +24,7 @@ export default controller({
|
|
|
24
24
|
|
|
25
25
|
// JSON 액션 — 객체를 반환하면 JSON 응답
|
|
26
26
|
async search() {
|
|
27
|
-
const { q } = this.params({ _row: {} as { q: string } }) // 검증
|
|
27
|
+
const { q } = this.params({ _row: {} as { q: string } }) // 타입만 · 런타임 검증 없음(스키마 파생 폼과 달리)
|
|
28
28
|
return { results: await Post.published().search(q).limit(10).all() }
|
|
29
29
|
},
|
|
30
30
|
})
|
|
@@ -506,7 +506,7 @@ import { SendWelcomeMail } from '../../../domain/jobs/sendWelcomeMail.js'
|
|
|
506
506
|
|
|
507
507
|
export default controller({
|
|
508
508
|
async new() {
|
|
509
|
-
return this.render('Auth/
|
|
509
|
+
return this.render('Auth/Signup', {})
|
|
510
510
|
},
|
|
511
511
|
async create() {
|
|
512
512
|
const { name, email, password } = this.params({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.5",
|
|
4
4
|
"description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"typescript": "^5.9.0",
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
30
|
"@gaonjs/async": "0.15.0",
|
|
31
|
-
"@gaonjs/config": "0.17.4",
|
|
32
31
|
"@gaonjs/core": "0.2.3",
|
|
33
|
-
"@gaonjs/
|
|
32
|
+
"@gaonjs/config": "0.17.4",
|
|
34
33
|
"@gaonjs/data": "0.17.0",
|
|
34
|
+
"@gaonjs/web": "0.19.4",
|
|
35
35
|
"@gaonjs/i18n": "0.2.2",
|
|
36
|
-
"@gaonjs/
|
|
36
|
+
"@gaonjs/mail": "0.3.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""
|