@gaonjs/cli 0.58.1 → 0.59.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/index.d.ts
CHANGED
|
@@ -100,5 +100,11 @@ export declare function readPortFlag(argv: readonly string[]): number | undefine
|
|
|
100
100
|
export declare function parseServeArgs(argv: readonly string[]): ServeCommandOptions;
|
|
101
101
|
/** `gaon dev` argv → DevCommandOptions. 포트 검증은 fail-loud(결정 240). */
|
|
102
102
|
export declare function parseDevArgs(argv: readonly string[]): DevCommandOptions;
|
|
103
|
+
/**
|
|
104
|
+
* 결정 448: 명령 공통 `--help`/`-h` 판정. `gaon test -- <인자>` 의 `--` 뒤는
|
|
105
|
+
* vitest 전달분이라 제외한다(패스스루 계약 유지 — `gaon test -- --help` 는
|
|
106
|
+
* vitest 의 도움말이지 gaon 의 도움말이 아니다).
|
|
107
|
+
*/
|
|
108
|
+
export declare function helpRequested(argv: readonly string[]): boolean;
|
|
103
109
|
/** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
|
|
104
110
|
export declare function runCli(argv: readonly string[], opts?: RunOptions): void;
|
package/dist/index.js
CHANGED
|
@@ -141,7 +141,7 @@ function renderHelp(version = VERSION) {
|
|
|
141
141
|
" gaon db seed domain/seed.ts 실행 (M8)",
|
|
142
142
|
" gaon --json 같은 정보를 JSON 으로 출력",
|
|
143
143
|
" gaon --version 버전 출력",
|
|
144
|
-
" gaon --help 이 도움말",
|
|
144
|
+
" gaon --help 이 도움말 (어느 명령 뒤에 붙여도 부팅 없이 이 도움말 · 결정 448)",
|
|
145
145
|
"",
|
|
146
146
|
" 문서: " + HOMEPAGE,
|
|
147
147
|
"",
|
|
@@ -360,6 +360,19 @@ export function parseDevArgs(argv) {
|
|
|
360
360
|
host,
|
|
361
361
|
};
|
|
362
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* 결정 448: 명령 공통 `--help`/`-h` 판정. `gaon test -- <인자>` 의 `--` 뒤는
|
|
365
|
+
* vitest 전달분이라 제외한다(패스스루 계약 유지 — `gaon test -- --help` 는
|
|
366
|
+
* vitest 의 도움말이지 gaon 의 도움말이 아니다).
|
|
367
|
+
*/
|
|
368
|
+
export function helpRequested(argv) {
|
|
369
|
+
const sepIdx = argv.indexOf("--");
|
|
370
|
+
const before = (flag) => {
|
|
371
|
+
const i = argv.indexOf(flag);
|
|
372
|
+
return i >= 0 && (sepIdx < 0 || i < sepIdx);
|
|
373
|
+
};
|
|
374
|
+
return before("--help") || before("-h");
|
|
375
|
+
}
|
|
363
376
|
/** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
|
|
364
377
|
export function runCli(argv, opts = {}) {
|
|
365
378
|
const version = opts.version ?? VERSION;
|
|
@@ -370,6 +383,13 @@ export function runCli(argv, opts = {}) {
|
|
|
370
383
|
// 명령이 추가될 때마다 또 빠지므로 여기서 공통화한다. loadDotEnv 는 이미
|
|
371
384
|
// 설정된 env 를 덮지 않고 파일이 없으면 조용히 지나가 멱등하다(재호출 안전).
|
|
372
385
|
loadDotEnv();
|
|
386
|
+
// 결정 448: `--help`/`-h` 는 **어느 명령에서든** 부팅·실행 대신 도움말을 낸다 —
|
|
387
|
+
// 종전엔 말미(명령 미매치 경로)에서만 처리해 `gaon serve --help`·`gaon hub --help`
|
|
388
|
+
// 가 실제 부팅을 시도했다(redis/NATS 미기동이면 접속 실패로 종료 · 실사용 보고).
|
|
389
|
+
if (helpRequested(argv)) {
|
|
390
|
+
process.stdout.write(renderHelp(version) + "\n");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
373
393
|
// `gaon dev` — 통합 개발 오케스트레이션(M9-C · v0.15 §13.5). Docker Compose
|
|
374
394
|
// 자동 기동 + .gaon 재생성 + serve·work·hub 자식(운영 3종 all-in-one · 결정 211)
|
|
375
395
|
// + tsc/vue-tsc watch + 서버 재시작 워처. SIGINT/SIGTERM 시 순서대로 정리
|
package/dist/scaffold/channel.js
CHANGED
|
@@ -39,6 +39,9 @@ export function channelScaffold(rawName, app, instance) {
|
|
|
39
39
|
` instance: true,`,
|
|
40
40
|
` // 인스턴스 단위 입장 판정 — ctx.instance 가 인스턴스 키(예: 매치 id·게시물 id)다.`,
|
|
41
41
|
` // 참가자 검증이 필요하면 DB 로 확인한다(예: 매치 참가자 테이블 조회).`,
|
|
42
|
+
` // 함정(결정 447): 숫자 키를 DB id 로 파싱해 검증한다면 **정규형 비교**까지 —`,
|
|
43
|
+
` // BigInt("042")·BigInt("0x2a") 도 42 라서, String(id) !== ctx.instance 면 거부해야`,
|
|
44
|
+
` // 같은 방의 유령 인스턴스(로스터에 안 보이는 우회 접속)를 막는다(agents/realtime.md §2.7).`,
|
|
42
45
|
` authorize(ctx) {`,
|
|
43
46
|
` return ctx.user != null // 로그인 사용자만 — 공개 관전형이면 이 훅을 지운다`,
|
|
44
47
|
` },`,
|
|
@@ -209,10 +209,19 @@ import { MatchPlayer } from '../../../domain/models/MatchPlayer.js'
|
|
|
209
209
|
export default channel({
|
|
210
210
|
instance: true, // ← 파라미터화 선언
|
|
211
211
|
async authorize(ctx) {
|
|
212
|
-
// ctx.instance = 매치 id — 인스턴스 단위 입장 판정(참가자만)
|
|
212
|
+
// ctx.instance = 매치 id — 인스턴스 단위 입장 판정(참가자만).
|
|
213
|
+
// 결정 447: 숫자 키는 **정규형 비교까지** — BigInt("042")·BigInt("0x2a") 도 42 라서
|
|
214
|
+
// 파싱만 하면 같은 매치의 "유령 인스턴스"(격리는 따로, DB 는 같은 방)가 열린다.
|
|
213
215
|
const u = ctx.user as { id: bigint } | null
|
|
214
216
|
if (!u) return false
|
|
215
|
-
|
|
217
|
+
let matchId: bigint
|
|
218
|
+
try {
|
|
219
|
+
matchId = BigInt(ctx.instance)
|
|
220
|
+
} catch {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
if (String(matchId) !== ctx.instance) return false // 비정규 표기("042"·"0x2a"·공백) 거부
|
|
224
|
+
return await MatchPlayer.where({ matchId, userId: u.id }).exists()
|
|
216
225
|
},
|
|
217
226
|
onMessage(ctx, data) {
|
|
218
227
|
// ctx.broadcast 는 자기 인스턴스(match:<id>)로 자동 스코프 — 다른 매치는 못 받는다
|
|
@@ -284,6 +293,13 @@ const draft = ref('')
|
|
|
284
293
|
- **인스턴스 키는 임의 문자열이다**(128자 이하) — 매치 id·게시물 id 같은
|
|
285
294
|
식별자를 그대로 쓴다. subject/KV 에는 identity 전체가 base64url 로 인코딩돼
|
|
286
295
|
들어가므로 특수문자 주입 걱정이 없다.
|
|
296
|
+
- **숫자 키를 DB id 로 파싱해 검증한다면 정규형 비교까지(결정 447).** 격리는
|
|
297
|
+
인스턴스 **문자열** 단위인데 `BigInt()`/`Number()` 파싱은 `"042"`·`"0x2a"`·
|
|
298
|
+
`" 42"` 를 전부 42 로 받으므로, 파싱 성공만 검사하면 인증 사용자가 같은 방의
|
|
299
|
+
**유령 인스턴스**를 열 수 있다 — 로스터·라이브에는 안 보이면서 `onMessage` 가
|
|
300
|
+
같은 DB row(방 42)에 기록을 남긴다. 위 match 예시처럼 파싱 후
|
|
301
|
+
`String(id) !== ctx.instance → false` 로 정규형만 통과시킨다(uuid·slug 처럼
|
|
302
|
+
문자열 그대로 조회하는 키는 해당 없음).
|
|
287
303
|
- **`ctx.instance` 는 타입으로 갈린다** — `instance: true` 채널의 훅에서는
|
|
288
304
|
`string`(항상 존재), 정적 채널에서는 `undefined`(string 으로 쓰면 컴파일 에러).
|
|
289
305
|
- **`presenceOf(member)` 는 역방향 조회다** — 멤버(`user:<id>`·`conn:<uuid>`)가
|
|
@@ -353,7 +369,11 @@ import { instancesOf } from 'gaonjs/async'
|
|
|
353
369
|
export default controller({
|
|
354
370
|
async show() {
|
|
355
371
|
// 영속 방이면 DB(Room.all())가, 일시적 방이면 instancesOf 가 첫 목록이다.
|
|
356
|
-
|
|
372
|
+
// 결정 446: 인원 수·메타가 필요하면 옵션으로 동봉한다 — 방마다 presenceList
|
|
373
|
+
// 를 따로 부르는 N+1 을 만들지 말 것(counts 는 프레즌스 키 단일 스캔).
|
|
374
|
+
const rooms = await instancesOf('match', { counts: true })
|
|
375
|
+
// rooms = [{ instance: '42', members: 3 }, …] · { meta: true, counts: true } 조합도 된다
|
|
376
|
+
return this.render('Lobby', { rooms })
|
|
357
377
|
},
|
|
358
378
|
})
|
|
359
379
|
```
|
|
@@ -368,11 +388,11 @@ export default channel({})
|
|
|
368
388
|
`room-closed` 를 반영한다 — 리스너의 `broadcast('lobby', …)` 단일 발행이 전
|
|
369
389
|
서버 로비 구독자에게 팬아웃된다(§2.5).
|
|
370
390
|
|
|
371
|
-
### 2.9 룸 프리미티브 — 정원·kick
|
|
391
|
+
### 2.9 룸 프리미티브 — 정원·kick·메타·입장순·멤버 이탈 (결정 444·445)
|
|
372
392
|
|
|
373
|
-
인스턴스 채널(§2.7) 위의 "방 운영" 표면
|
|
374
|
-
(모더레이터 강퇴·방 인원 제한·방
|
|
375
|
-
|
|
393
|
+
인스턴스 채널(§2.7) 위의 "방 운영" 표면 5종. 전부 도메인 중립이다 — 채팅
|
|
394
|
+
(모더레이터 강퇴·방 인원 제한·방 규칙·방장 승계)과 게임(안티치트 축출·매치
|
|
395
|
+
정원·매치 설정·호스트 승계)이 같은 프리미티브를 쓴다.
|
|
376
396
|
|
|
377
397
|
**① 정원 `maxMembers` — 허브 원자 판정.** 정의에 선언하면 초과 입장이
|
|
378
398
|
`4409` 로 거부된다(허브가 전역 로스터 단일 권위로 **동시 입장 race 없이**
|
|
@@ -424,7 +444,14 @@ await kick('room', `user:${userId}`, { instance: roomId, reason })
|
|
|
424
444
|
async authorize(ctx) {
|
|
425
445
|
const u = ctx.user as { id: bigint } | null
|
|
426
446
|
if (!u) return false
|
|
427
|
-
|
|
447
|
+
let roomId: bigint
|
|
448
|
+
try {
|
|
449
|
+
roomId = BigInt(ctx.instance)
|
|
450
|
+
} catch {
|
|
451
|
+
return false
|
|
452
|
+
}
|
|
453
|
+
if (String(roomId) !== ctx.instance) return false // 정규형만(§2.7 유령 인스턴스 · 결정 447)
|
|
454
|
+
return !(await RoomBan.where({ roomId, userId: u.id }).exists())
|
|
428
455
|
}
|
|
429
456
|
```
|
|
430
457
|
|
|
@@ -455,25 +482,60 @@ const rooms = await instancesOf('match', { meta: true }) // 로
|
|
|
455
482
|
`presenceList`/`ctx.presence()`/클라 `members` 가 **입장순 정렬을 보장**
|
|
456
483
|
하므로 "가장 먼저 들어온 남은 멤버" = `[0]` 이 전 서버 어디서나 결정적이다.
|
|
457
484
|
|
|
458
|
-
|
|
459
|
-
(
|
|
485
|
+
**⑤ `MemberLeft` — 멤버 이탈의 race-free 앵커(결정 445).** 인스턴스 채널에서
|
|
486
|
+
멤버가 **완전히 떠나면**(마지막 연결 소멸 — 멀티탭 중간 탭 닫힘은 침묵 · 서버
|
|
487
|
+
프로세스 死 포함) 허브가 **로스터에서 제거한 뒤** 발행한다. 페이로드에 "누가
|
|
488
|
+
나갔는가 + **제거가 반영된** 잔존 로스터(입장순)"가 실리므로, "떠난 뒤의
|
|
489
|
+
로스터로 한 곳에서 결정" 하는 로직의 정본 앵커다. `InstanceOpened/Closed` 와
|
|
490
|
+
같은 전달(워커 하나만 처리 · at-least-once — 핸들러 멱등):
|
|
460
491
|
|
|
461
492
|
```ts
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
493
|
+
export default on(MemberLeft, async ({ channel, instance, member, roster }) => {
|
|
494
|
+
// roster = 제거 반영 후 잔존 멤버 [{ id, joinSeq? }] · joinSeq 오름차순.
|
|
495
|
+
// 빈 배열 = 마지막 이탈(InstanceClosed 도 발행되지만 컨슈머 축이 달라
|
|
496
|
+
// 상호 순서는 보장 없음 — 빈 로스터 자체를 마지막 이탈 신호로 쓰라).
|
|
497
|
+
})
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
- **채널 `onLeave` 안 `ctx.presence()` 로 이탈 후 로스터를 읽지 말 것** — leave
|
|
501
|
+
는 훅 앞에서 **발신**될 뿐 KV 반영은 허브 비동기라, 떠나는 멤버 본인이 아직
|
|
502
|
+
로스터에 보일 수 있다(로컬에선 대개 허브가 이겨 테스트는 통과하고, 부하·순단
|
|
503
|
+
에서 뒤집히는 전형적 race — rooms 샘플 실측). 게다가 웹서버가 통째로 죽으면
|
|
504
|
+
`onLeave` 는 아예 돌지 않는다. 둘 다 `MemberLeft` 가 닫는다.
|
|
505
|
+
- **발화는 이탈만·인스턴스 채널만이다** — 입장 후 로직은 `onJoin`(연결)·
|
|
506
|
+
`InstanceOpened`(첫 점유)가 담당하고, 정적 채널은 전역 churn 고빈도 + 소멸
|
|
507
|
+
앵커 부재로 발화하지 않는다(결정 442 스코프 동형).
|
|
508
|
+
|
|
509
|
+
**방장(호스트) 승계는 앱 도메인 패턴이다** — 프레임웍은 결정적 순서(joinSeq)와
|
|
510
|
+
race-free 앵커(MemberLeft)만 준다("owner" 프리미티브 없음):
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
// domain/listeners/onRoomMemberLeft.ts — 채팅 방장 / 게임 호스트 공통 골격.
|
|
514
|
+
// 워커 하나만 처리하므로 멀티서버 동시 이탈에도 승계가 한 곳에서 결정된다.
|
|
515
|
+
import { on, MemberLeft, broadcast } from 'gaonjs/async'
|
|
516
|
+
import { Room } from '../models/Room.js'
|
|
517
|
+
|
|
518
|
+
export default on(MemberLeft, async ({ channel, instance, member, roster }) => {
|
|
519
|
+
if (channel !== 'room') return
|
|
520
|
+
if (roster.length === 0) return // 마지막 이탈 — 닫힘은 InstanceClosed 몫
|
|
521
|
+
const room = await Room.where('key', '=', instance).first()
|
|
522
|
+
if (!room) return
|
|
523
|
+
if (roster.some((m) => m.id === `user:${String(room.ownerId)}`)) return // 방장 건재
|
|
524
|
+
const next = roster[0] // 가장 먼저 들어온 남은 멤버(입장순 보장)
|
|
525
|
+
if (!next.id.startsWith('user:')) return
|
|
526
|
+
// 조건부 갱신(멱등) — at-least-once 재전달·경합에서 두 번 승격되지 않는다.
|
|
527
|
+
const changed = await Room.where('id', '=', room.id)
|
|
528
|
+
.where('ownerId', '=', room.ownerId)
|
|
529
|
+
.updateAll({ ownerId: BigInt(next.id.slice('user:'.length)) })
|
|
530
|
+
if (changed > 0) broadcast('room', { type: 'owner-changed', owner: next.id }, { instance })
|
|
467
531
|
})
|
|
468
|
-
// 승계(owner 이탈 감지 시 — 도메인 액션·리스너 어디서든):
|
|
469
|
-
const roster = await presenceList('match', { instance })
|
|
470
|
-
const next = roster[0] // 가장 먼저 들어온 남은 멤버
|
|
471
|
-
if (next) {
|
|
472
|
-
await setInstanceMeta('match', { owner: next.id }, { instance })
|
|
473
|
-
broadcast('match', { type: 'owner-changed', owner: next.id }, { instance })
|
|
474
|
-
}
|
|
475
532
|
```
|
|
476
533
|
|
|
534
|
+
영속 방(위 예시)은 **DB 컬럼(rooms.ownerId)이 owner 의 정본**이다 — 메타
|
|
535
|
+
(`setInstanceMeta({ owner })`)는 닫힘 시 소멸하는 표시층이라, 방 row 가 남는
|
|
536
|
+
도메인이면 DB 로 두고 메타는 로비 표시 등에만 쓴다. 일시적 방(row 없는 매치)
|
|
537
|
+
이면 메타 owner 로 충분하다.
|
|
538
|
+
|
|
477
539
|
### 3. 프레즌스
|
|
478
540
|
|
|
479
541
|
`ctx.presence()` 는 **전 서버의** 현재 접속자를 돌려준다. 목록의 권위는
|
|
@@ -757,9 +819,16 @@ export default channel({
|
|
|
757
819
|
- **방 생성/삭제를 허브·채널 훅에서 직접 DB 로 밀지 말 것** — 훅(onJoin/onLeave)은
|
|
758
820
|
연결 단위라 첫/마지막 판정이 서버 로컬에 갇히고(멀티서버에서 틀림), 허브는
|
|
759
821
|
도메인을 모른다. 전역 첫/마지막은 생명주기 이벤트(§2.8)가 정답 경로다.
|
|
760
|
-
훅의 단위 경계 3
|
|
761
|
-
`ctx.connectionId` 로 구분) / presence 델타 = 멤버(첫·마지막 연결
|
|
762
|
-
|
|
822
|
+
훅의 단위 경계 3층 + 멤버층 서버 이벤트: **onJoin/onLeave = 연결(멀티탭이면
|
|
823
|
+
연결마다 · `ctx.connectionId` 로 구분) / presence 델타 = 멤버(첫·마지막 연결 ·
|
|
824
|
+
클라 통지) / `MemberLeft` = 멤버 이탈의 서버측 앵커(§2.9 ⑤ · 결정 445 ·
|
|
825
|
+
로스터 반영 후 · 워커 1회 처리) / InstanceOpened·Closed = 인스턴스(전역 첫
|
|
826
|
+
점유·점유 0)**.
|
|
827
|
+
- **이탈 후 로스터 판정을 `onLeave` + `ctx.presence()` 로 조립하지 말 것(결정 445)** —
|
|
828
|
+
leave 는 훅 앞에서 발신될 뿐 KV 반영은 허브 비동기라 이탈자 본인이 로스터에
|
|
829
|
+
잔존할 수 있고(로컬에선 대개 통과하다 부하·순단에서 뒤집히는 race), 서버
|
|
830
|
+
프로세스 死 경로에서는 훅이 아예 안 돈다. 방장 승계처럼 "떠난 뒤의 로스터로
|
|
831
|
+
결정" 하는 로직은 `on(MemberLeft, …)` 리스너가 정본이다(§2.9 ⑤).
|
|
763
832
|
- **`authorize` 안 `presenceList` 로 정원을 검사하지 말 것** — 웹서버 로컬 판정이라
|
|
764
833
|
동시 입장 race 에서 초과 입장이 뚫린다. 정원은 **`maxMembers` 선언**(§2.9 ·
|
|
765
834
|
허브 원자 판정 · 4409)이 정본이다.
|
|
@@ -803,6 +872,10 @@ export default channel({
|
|
|
803
872
|
| 결정 440 | 파라미터화(인스턴스) 채널(§2.7) — `instance: true` 선언 · identity = `<이름>:<인스턴스>`(Phoenix topic 모델) · 전송 subject·프레즌스·인가·broadcast 네 축 identity 격리 · 역방향 인덱스(`member.<멤버>.<채널>`)로 `presenceOf(member)` 한 번 스캔 · `presenceList(name, {instance})` 서버 개시 로스터 · 선언·연결 부정합 = `4400` fail-loud(재연결 없음) · doctor `channel-instance-authorize` 경고 · 정적 채널은 identity=이름 그대로(와이어·KV 무변경) |
|
|
804
873
|
| 결정 442 | 소켓 앱 DX 표면 완성(§2.8) — 인스턴스 생명주기 이벤트 `InstanceOpened`/`InstanceClosed`(허브 발행 · 활성 인스턴스 인덱스 `chan.<이름>.<인스턴스>` 가 dedup 앵커 · 리스너 durable 공유로 워커 하나만 처리) · `instancesOf(name)` 채널→활성 인스턴스 목록(로비 첫 렌더) · 로비 = 일반 채널 + `broadcast` 패턴(전용 API 신설 없음) · `gaon g channel <이름> [--instance]` 스캐폴드(정의 + 컴포저블) |
|
|
805
874
|
| 결정 444 | 룸 프리미티브 Wave 2(§2.9) — 정원 `maxMembers`(허브 원자 판정 joinAck · 초과 `4409` 종단 · 멤버 수 기준 멀티탭 통과) · `kick(name, member, {instance, reason})`(userSubject 제어 봉투 · `4403` 종단 + `onKicked` · 반환 = present 수 · 마지막 멤버면 closed 자동) · ban = 3단 문서 패턴(DB + authorize + kick · 프리미티브 없음) · 인스턴스 메타(`instanceMeta` 훅 = open 앵커 1회 기록 · `setInstanceMeta` LWW 갱신 · 닫힘 자동 삭제 · 4096B · 정적 채널 미지원) · `joinSeq` 입장 순번(허브 부여 · `presenceList`/`members` 입장순 정렬 보장 · 방장 승계 패턴 기반) · `ctx.connectionId` |
|
|
875
|
+
| 결정 445 | `MemberLeft` 멤버 이탈 이벤트(§2.9 ⑤) — 허브가 로스터 **제거 후** 발행(제거 반영 로스터 동봉 · 입장순) · 마지막 연결에서만(멀티탭 침묵) · graceful·synced 대사·서버 死(cleanupServer)·failover 회수 전 경로 단일 수렴점(delMember) · 인스턴스 채널 한정·이탈만(444(F) 볼륨 우려 수용) · 워커 1회 처리(442 전달 동형) · 방장 승계 정본 앵커("onLeave + ctx.presence() 재조회" 패턴은 leave 발신≠반영 race + 서버 死 미커버로 반정본 명문화 — rooms 샘플 실측) |
|
|
876
|
+
| 결정 446 | `instancesOf(name, { counts: true })`(§2.8) — 인스턴스별 **멤버 수** 동봉(프레즌스 키 단일 스캔 · presenceStats 동형) · `{ meta: true, counts: true }` 조합 지원 · 로비 인원 수 per-인스턴스 `presenceList` N+1 제거 · 기존 시그니처 불변 |
|
|
877
|
+
| 결정 447 | 인스턴스 키 정규형 비교 정본화(§2.7·§2.9 ban) — 숫자 키를 `BigInt()`/`Number()` 로 파싱해 검증하는 authorize 는 `String(id) !== ctx.instance → false` 정규형 가드까지(비정규 표기 "042"·"0x2a" 가 로스터에 안 보이는 유령 인스턴스로 같은 DB row 에 기록을 남기는 우회 차단) · 프레임웍 자동 정규화 기각(키는 앱 정의 불투명 문자열 — uuid·slug 는 파싱 무관) |
|
|
878
|
+
| 결정 448 | CLI 명령 공통 `--help`(`packages/cli`) — `gaon serve --help`·`gaon hub --help` 가 부팅을 시도하던 것을 디스패치 전 공통 처리로 봉합 · `gaon test -- <인자>` 의 `--` 뒤는 패스스루 보존 |
|
|
806
879
|
|
|
807
880
|
## `@gaonjs/seal` 켠 앱의 채널
|
|
808
881
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.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/async": "0.
|
|
36
|
-
"@gaonjs/config": "0.25.5",
|
|
37
|
-
"@gaonjs/data": "0.25.3",
|
|
38
|
-
"@gaonjs/i18n": "0.3.1",
|
|
35
|
+
"@gaonjs/async": "0.22.0",
|
|
39
36
|
"@gaonjs/core": "0.3.0",
|
|
37
|
+
"@gaonjs/config": "0.25.6",
|
|
38
|
+
"@gaonjs/i18n": "0.3.1",
|
|
39
|
+
"@gaonjs/data": "0.25.3",
|
|
40
40
|
"@gaonjs/mail": "0.5.1",
|
|
41
|
-
"@gaonjs/web": "0.31.
|
|
41
|
+
"@gaonjs/web": "0.31.2"
|
|
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})\""
|