connectbase-client 3.25.0 → 3.25.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,1911 @@
1
+ # Changelog
2
+
3
+ 본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
4
+ 버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
5
+
6
+ ## [3.25.1] - 2026-05-27
7
+
8
+ ### Fixed — 3.25.0 회귀 복구 + heartbeat 헬퍼 정리
9
+
10
+ 3.25.0 이 NativeBridge / inApp 우회 / deploy timeout 을 추가하면서, 같은 시기 3.24.0 에 들어가 있던
11
+ incremental deploy(manifest 기반 +/− diff) 와 `signUp` / `startCentralOAuth` 흐름을 통째로 누락한 채
12
+ release 됐다. 본 패치는 두 라인을 머지해 3.24.0 의 동작을 회복하면서 3.25.0 의 신규 기능을 모두 유지한다.
13
+
14
+ - **CLI deploy** — `fullDeploy` 만 호출되던 회귀를 닫고 `incrementalDeploy` + `tryFetchManifest` + `computeDeployDiff` + `handleDeployResponse` 복원.
15
+ - Dev 는 항상 전량(`/deploy/dev`), Prod 는 manifest 기반 `+upsert/-delete` 전송, 변경 0 이면 업로드 skip.
16
+ - 409 revision conflict 자동 1회 재시도.
17
+ - 두 경로 모두 `computeDeployTimeout(totalBytes, override)` 결과를 `makeRequest` 의 `timeoutMs` 로 전달.
18
+ - `startUploadHeartbeat()` 헬퍼로 분리해 incremental / full 양쪽에서 TTY heartbeat 표시 (CI 환경은 무음).
19
+ - `VERSION` 을 빌드된 `package.json` 에서 읽도록 복원 (`getPackageVersion()`).
20
+ - **OAuth** — `signUp(provider, callbackUrl, state)` 와 `startCentralOAuth(intent)` 가 다시 export.
21
+ - `signIn` / `signUp` 모두 `startCentralOAuth` 를 거치며, 내부에서 ① NativeBridge 감지 ②
22
+ 3rd-party 인앱 브라우저(`detectInAppBrowser` + `escapeToExternalBrowser`) 처리 ③ 일반 웹 redirect
23
+ 순으로 분기. 3.25.0 에서 `signIn` 만 inApp 처리를 받던 비대칭을 해소.
24
+ - `signInWithPopup` 도 같은 3-way 분기 + `options.intent` 유지.
25
+ - **Exports** — `detectInAppBrowser`, `escapeToExternalBrowser` 외에 3.24.0 의 `TokenPersistence` 타입과
26
+ `GameError` 도 함께 re-export (3.25.0 에서 export 라인이 충돌 머지로 일부 빠져있던 회귀).
27
+
28
+ ### Notes — 누락된 CHANGELOG 보강
29
+
30
+ 3.24.0 / 3.25.0 은 CHANGELOG 엔트리 없이 publish 됐다. 향후 retro 엔트리를 보강할 수 있으나, 본 릴리스는
31
+ 3.25.0 → 3.25.1 의 diff 만 다룬다.
32
+
33
+ ## [3.23.0] - 2026-05-26
34
+
35
+ ### Fixed — OAuth 표준 흐름 페이지 리로드 후 세션 유실 (platform-issue 019e638d)
36
+
37
+ 문서의 OAuth redirect 표준 예제 그대로 따라가도 **콜백 → 메인 페이지** 전환 직후 `cb.auth.getMe()` 가 401 으로 떨어져 비개발자 사용자가 영원히 로그인 루프에 갇히던 회귀를 3개 race + 1개 충돌 가드로 동시에 닫는다.
38
+
39
+ **(1) Race A — callback fire-and-forget cookie 부트스트랩**
40
+ `getCallbackResult()` / `exchangeCodeFromCallback()` 가 `setTokens` 직후 `bootstrapRefreshCookie()` 를 fire-and-forget 으로 호출했다. 표준 예제처럼 `window.location.href='/'` 가 즉시 발화하면 fetch 가 abort 되어 `cb_member_refresh_token` cookie 가 발급되지 않았다. 모바일·느린 네트워크에서 특히 잘 깨졌다.
41
+ - **변경**: `getCallbackResult()` 시그니처를 동기 → `Promise` 로 변경 (BREAKING — prelaunch 단계라 backward-compat 가드 생략). `exchangeCodeFromCallback()` / popup 핸들러도 cookie 부트스트랩을 *await* 한 뒤에 resolve. 호출자가 `await cb.oauth.getCallbackResult()` 후 navigation 하면 cookie 가 안전하게 저장된 상태가 보장된다.
42
+
43
+ **(2) Race B — 메인 페이지 entry race**
44
+ ConnectBase 인스턴스 생성 시 `tryRestoreSessionFromCookie()` 가 fire-and-forget 으로 진행되는데, 사용자 코드의 첫 인증 호출(`getMe()` 등) 이 그 promise 보다 먼저 발화하면 메모리 토큰 빈 채로 401 받았다.
45
+ - **변경**: 부팅 시 시작된 복구 promise 를 HttpClient 에 등록해, `prepareHeaders` 가 인증 호출 직전에 1회 await. 표준 예제 무수정으로 race 가 닫힌다.
46
+
47
+ **(3) 401 자동 복구**
48
+ 인증 호출이 401 받으면 cookie 기반 복구를 *한 번* 시도하고 retry. cookie 가 살아 있는 사용자의 첫 호출이 화면 401 으로 깨지지 않게 한다 (cookie 복구 실패 시엔 원본 401 그대로 throw — 무한 retry 차단).
49
+
50
+ **(4) Backend strict X-Public-Key dispatch (충돌 C)**
51
+ 같은 `.connectbase.world` 루트의 콘솔 admin refresh cookie 가 `credentials:'include'` 로 SDK 호출에 함께 첨부되어, SDK 가 콘솔 User JWT 를 silent reject 하는 케이스 차단. backend `/v1/auth/re-issue` 에서 cross-source cookie fallback 을 제거 — SDK 호출(X-Public-Key 있음) 은 `cb_member_refresh_token` 만, 콘솔 호출은 `refresh_token` 만 사용한다. 한쪽이 없으면 401 로 깔끔히 떨어뜨려 SDK 가드(`isConsolePlatformToken`) 가 더 이상 트리거되지 않는다.
52
+
53
+ ### Breaking
54
+
55
+ - `cb.oauth.getCallbackResult()` 가 `Promise` 를 반환합니다. 호출자는 반드시 `await` 해야 합니다. 컴파일 타임에 잡히지만, 동기 호출 코드는 콜백 결과를 `Promise<...>` 로 받아 `result.error` 가 항상 undefined 로 보입니다.
56
+ - **마이그레이션**: `const result = cb.oauth.getCallbackResult()` → `const result = await cb.oauth.getCallbackResult()`. React `useEffect` 안에서는 IIFE 패턴: `;(async () => { const result = await cb.oauth.getCallbackResult(); ... })()`.
57
+
58
+ ### Tests
59
+
60
+ - `test/http-race-recovery.test.ts` — race B / 401 auto-recovery / skipAuth 보존 5 케이스
61
+ - `test/oauth-callback-cookie-bootstrap.test.ts` — `await` 시그니처 + bootstrap-before-result 보장 추가
62
+
63
+ ## [3.22.1] - 2026-05-26
64
+
65
+ ### Documentation
66
+
67
+ README 에 3.22.0 에서 추가된 공개 API 3종을 정식 섹션으로 문서화. 코드 동작 변경 없음.
68
+
69
+ - **Key Types** 섹션에 `Server-side admin context` 추가 — `new ConnectBase({ publicKey, secretKey })` 가 admin 헤더(`X-Public-Key` + `Authorization: Bearer cb_sk_*`) 를 첨부하고 서버 `OptionalAdminSecretKey` 미들웨어가 RLS 를 우회한다는 점을 명시.
70
+ - **Authentication** 섹션에 `cb.auth.adminUpdateMember(memberID, fields)` 예제 + `secretKey` 미설정 시 throw / self-update 거절 동작 + `role` 이 RLS `auth.role` 의 backing field 임을 명시.
71
+ - **Server Functions** 섹션 신설 (Realtime 과 Endpoint 사이) — `cb.functions.invoke` + `cb.functions.getWebhookURL` + `http_trigger_auth` 3종(`none` / `public_key` / `secret_key`) + raw body / 헤더 forward / 10MB 한도 / Discord interactions 응답 예제.
72
+
73
+ ## [3.22.0] - 2026-05-25
74
+
75
+ ### Added — 서버사이드 admin (cb_sk_) 권한으로 data CRUD + 멤버 role 관리 (platform-issue 019e5a04, 019e59ca)
76
+
77
+ #### `Authorization: Bearer cb_sk_*` 자동 첨부
78
+
79
+ `ConnectBase({ publicKey, secretKey })` 처럼 publicKey 와 secretKey 를 동시에 보유한
80
+ 인스턴스는 모든 요청에 publicKey 는 `X-Public-Key` (앱 식별), secretKey 는
81
+ `Authorization: Bearer cb_sk_*` (권한 격상) 두 헤더를 함께 전송한다.
82
+
83
+ 서버는 새 `OptionalAdminSecretKey` 미들웨어가 cb_sk_ 를 검증하고 user 가 해당 앱에
84
+ 대한 권한을 가지는지 (UserAndAppRole) 확인한 뒤 RLS 우회 컨텍스트를 set 한다.
85
+ 서버사이드 sync / 관리 스크립트가 RLS 규칙 `.write: false` 를 우회해 데이터 CRUD
86
+ (`updateData`, `deleteData`, `createData`, `queryData`, `getData`) 를 수행할 수 있다.
87
+
88
+ #### `cb.auth.adminUpdateMember(memberID, fields)` 추가
89
+
90
+ RLS 평가 변수 `auth.role` 의 backing field 인 `member.role` 을 set 할 수 있는 SDK
91
+ 경로. self-update 가 아닌 admin 권한 호출이므로 secretKey 가 필수 (없으면 throw).
92
+ `{ nickname, role, custom_data }` 모두 PATCH 방식.
93
+
94
+ ```typescript
95
+ const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' })
96
+ await cb.auth.adminUpdateMember('member-uuid', { role: 'admin' })
97
+ ```
98
+
99
+ ### Added — Function raw HTTP webhook 헬퍼 (platform-issue 019e597c)
100
+
101
+ 함수가 `http_trigger_enabled=true` 로 생성/수정되면 외부 SaaS (Discord / Stripe /
102
+ GitHub / Slack Events / Notion) 가 raw HTTP webhook 으로 호출할 수 있다. body 는
103
+ wrap 없이 raw bytes 그대로 함수 핸들러에 전달 (Ed25519/HMAC 서명 검증 호환), 모든
104
+ 헤더 forward, method/path/query 보존. 함수가 `{statusCode, headers, body}` 를
105
+ 반환하면 그대로 HTTP 응답에 매핑.
106
+
107
+ ```typescript
108
+ const url = cb.functions.getWebhookURL('function-id')
109
+ // → https://api.connectbase.world/v1/public/functions/<id>/webhook
110
+ // Discord Interactions Endpoint URL / Stripe webhook endpoint 에 직접 등록.
111
+ ```
112
+
113
+ ## [3.21.1] - 2026-05-24
114
+
115
+ ### Fixed — `signInWithPopup` 콜백 cookie 부트스트랩 누락 회귀 (platform-issue 019e5922)
116
+
117
+ popup 콜백은 `*.connectbase.world` 중앙 도메인에서 발생하므로 부모 window 도메인
118
+ (예: `fream-note.com`) 에 `cb_member_refresh_token` cookie 가 직접 발급되지 않는다.
119
+ 기존 `signInWithPopup` 메시지 핸들러는 `setTokens()` 만 호출하고
120
+ `bootstrapRefreshCookie()` 를 호출하지 않아, `persistence='none'` + 페이지 새로고침 시
121
+ 메모리 토큰이 사라지고 같은 root domain 의 콘솔 cookie 가 cross-origin 으로 따라오는
122
+ 환경에서는 `/v1/auth/re-issue` 가 fallback 으로 콘솔 User JWT 를 발급해 화면이 깨졌다.
123
+
124
+ 다른 콜백 경로(`getCallbackResult` / `exchangeCodeFromCallback`) 와 동일하게
125
+ `bootstrapRefreshCookie()` 호출을 추가해 부모 window 도메인의 member cookie 를
126
+ 명시적으로 부트스트랩한다. `await` 하지 않는 best-effort 호출이라 호출자 흐름은 변하지 않는다.
127
+
128
+ 회귀 가드: `oauth-callback-cookie-bootstrap.test.ts` 에 `signInWithPopup` 케이스 추가.
129
+
130
+ ## [3.21.0] - 2026-05-23
131
+
132
+ ### Added — `CB_*_URL` 환경변수 폴백
133
+
134
+ ConnectBase 생성자가 `baseUrl` / `socketUrl` / `webrtcUrl` / `videoUrl` / `gameUrl` 을
135
+ 결정할 때 우선순위가 ① config 명시값 → ② `CB_*_URL` 환경변수 → ③ 공개 기본값 순서로
136
+ 적용된다.
137
+
138
+ 용도: 클러스터 내부에서 실행되는 서버리스 함수가 외부 LB(공개 URL)로 hairpin 하지 않고
139
+ 내부 service URL 로 직접 연결하도록. 함수 코드 변경 없이 function-server 가
140
+ `CB_BASE_URL` / `CB_SOCKET_URL` 등을 주입하는 것으로 동작.
141
+
142
+ 브라우저 환경(process undefined) 에서는 env 가 항상 undefined — 기존 동작과 동일.
143
+
144
+ ## [3.20.1] - 2026-05-22
145
+
146
+ ### Fixed — `AgenticSearchProgress` 타입 export 누락
147
+
148
+ 3.20.0 의 CHANGELOG 는 `AgenticSearchProgress` 를 신규 export 타입으로 명시했으나,
149
+ 패키지 진입점(`src/index.ts`)의 export 구문에서 누락되어
150
+ `import type { AgenticSearchProgress } from 'connectbase-client'` 가 `TS2614` 로
151
+ 실패했다. export 목록에 추가하여 `onSearching` 콜백을 타입 안전하게 쓸 수 있게 했다.
152
+
153
+ - 런타임/동작 변경 없음 — 타입 export 누락만 수정.
154
+ - 우회용으로 쓰던 `NonNullable<AIStreamChunk['searching']>` 추출은 더 이상 필요 없다.
155
+
156
+ ## [3.20.0] - 2026-05-22
157
+
158
+ ### Added — Agentic 검색 진행 실시간 중계 (`chatStream` 의 `onSearching`)
159
+
160
+ `cb.ai.chatStream({ agentic: true })` 시 agentic 다중검색의 진행 상황을 받는
161
+ `onSearching` 콜백을 추가했다. 검색어 생성(`query_generation`) → 검색 실행
162
+ (`searching`, 생성된 쿼리 포함) → 종료(`complete`, 라운드 수·결과 수)의 각 단계가
163
+ 실시간으로 전달되어 "검색 중…" 진행 UI 를 구성할 수 있다.
164
+
165
+ - 신규 타입 `AgenticSearchProgress` export — `phase` / `round` / `queries` /
166
+ `results` / `rounds`.
167
+ - 종료 이벤트(`phase: 'complete'`)의 `rounds` 로 agentic 이 실제 몇 라운드
168
+ 수행됐는지(0 = 폴백) 판별 가능 — `knowledge.search` 응답의 `agentic_rounds`
169
+ 와 동일한 신호.
170
+ - 기존 동작 영향 없음: agentic 미사용 시 `onSearching` 은 호출되지 않는다.
171
+
172
+ ## [3.19.0] - 2026-05-22
173
+
174
+ ### Added — AI 추론(reasoning) 스트리밍 (`chatStream` 의 `onReasoning`)
175
+
176
+ 추론형 모델의 추론 과정을 스트리밍으로 받는 `onReasoning` 콜백을 `cb.ai.chatStream` 에
177
+ 추가했다. 게이트웨이가 업스트림(openai 계열)의 `delta.reasoning` 을 토큰 청크의
178
+ `reasoning` 필드로 중계한다. 추론 델타는 본문(`content`)이 비어 있어 `onReasoning` 을
179
+ 쓰지 않는 구형 코드에는 영향이 없다 (하위호환).
180
+
181
+ ### Added — Agentic 검색 수행 신호 (`KnowledgeSearchResponse.agentic`)
182
+
183
+ `cb.knowledge.search` 응답에 `agentic` / `agentic_rounds` 필드를 추가했다. `agentic: true`
184
+ 를 보냈더라도 AI provider 미설정·LLM 오류로 단일 키워드 검색에 폴백되면 `agentic` 이
185
+ `false` 로 와, agentic 옵션이 실제로 수행됐는지 폴백됐는지 판별할 수 있다.
186
+
187
+ ### Note — npm 패키지 동기화
188
+
189
+ `onReasoning` 과 `knowledge.updateDocument`(아래 3.18.0 항목)는 코드에는 반영됐으나
190
+ npm 3.18.0 패키지에는 누락된 상태였다. 3.19.0 에 모두 정상 포함된다.
191
+
192
+ ## [3.18.0] - 2026-05-21
193
+
194
+ ### Added — Knowledge Base 문서 수정 (`knowledge.updateDocument`)
195
+
196
+ 기존 KB 문서를 수정하는 `cb.knowledge.updateDocument(kbID, docID, data)` 메서드를 추가했다.
197
+ `content` / `file_content` / `metadata` 중 하나라도 포함하면 서버가 전체 재색인(기존 청크
198
+ 삭제 → 재청킹 → 재색인)을 수행하며, `name` 만 보내면 재색인 없이 문서 라벨만 변경된다.
199
+ 재색인 시 서버는 `status='pending'` 으로 즉시 응답하고 색인은 백그라운드로 진행한다
200
+ (`addDocument` 와 동일).
201
+
202
+ - 신규 타입 `UpdateDocumentRequest` export.
203
+ - REST `PUT /v1/public/knowledge-bases/:kbID/documents/:docID` 에 대응.
204
+
205
+ ### Note — Knowledge Base 색인 과금
206
+
207
+ 서버에서 KB 문서 색인이 사용량 과금 대상이 되었다. 문서 생성·수정(재색인) 시 색인 토큰이,
208
+ 색인된 청크 보관 용량이 사용량으로 집계된다. SDK 사용 방식에는 변화가 없으며 BM25 키워드
209
+ 검색 자체는 외부 임베딩 API 비용이 없다.
210
+
211
+ ## [3.17.2] - 2026-05-20
212
+
213
+ ### Fixed — 콘솔 도메인 세션 쿠키 누수로 인한 앱 공개 API 401
214
+
215
+ `*.web.connectbase.world` 서브도메인에 배포된 앱은 콘솔 도메인(`connectbase.world`)의
216
+ 세션 쿠키를 `.connectbase.world` 스코프로 공유받는다. 로그인 기능이 없는 앱에서 개발자가
217
+ ConnectBase 콘솔에 로그인된 브라우저로 자기 앱을 열면, `autoRestoreSession` 이 `/v1/auth/re-issue`
218
+ 로부터 콘솔 User JWT 를 복구해 앱 공개 API(`/v1/public/*`) 요청의 `Authorization` 헤더에
219
+ 첨부했고, 서버가 이를 앱 멤버 토큰으로 검증하려다 실패해 **401** 이 발생했다. 일반 방문자에겐
220
+ 재현되지 않고 개발자가 자기 앱을 테스트할 때만 터지는 형태였다 (platform-issue `019e459c`).
221
+
222
+ 수정:
223
+ - `/v1/auth/re-issue` 가 콘솔/플랫폼 토큰(`role: User` / `AdminInvite`)을 돌려주면 세션으로
224
+ 채택하지 않는다 — 실패가 아닌 "앱 세션 없음" 으로 silent 처리.
225
+ - 메모리의 access token 이 콘솔/플랫폼 토큰이면 `Authorization` 헤더에 첨부하지 않는다 —
226
+ `X-Public-Key` 공개 키 인증으로 폴백.
227
+ - 앱 세션 토큰(`AppMember` / `Guest`)과 디코드 불가한 토큰은 영향 없음 — 콘솔 토큰으로
228
+ 확정될 때만 거부하므로 정상 세션을 잘못 폐기하지 않는다.
229
+
230
+ 로그인 기능이 없는 앱은 `autoRestoreSession: false` 로 명시하면 이 세션 복구 자체를 건너뛸 수 있다.
231
+
232
+ ## [3.17.1] - 2026-05-19
233
+
234
+ ### Docs — README 에 Knowledge Base (RAG) 섹션 신설
235
+
236
+ 기존 README 의 API Reference 가 game / auth / database / storage / realtime / endpoint /
237
+ push / webrtc / payments / support 만 다루고 knowledge 섹션이 통째로 누락된 상태였음.
238
+ npm 페이지에서 SDK 사용자가 RAG 기능 존재를 인지하지 못하는 누락 보강.
239
+
240
+ 추가:
241
+ - API Reference 에 "Knowledge Base (RAG)" 섹션 — `addDocument` (text/url) /
242
+ `addDocumentFromFile` (PDF/DOCX/text, 50MB) / `listDocuments` / `deleteDocument` /
243
+ `search` (BM25 + Agentic) / `searchGet` 예제
244
+ - 파일 업로드 제약 (지원/미지원 MIME, 50MB, OCR 미지원) 박스
245
+ - 사용자별 격리 (AppMember JWT 기반 metadata.user_id 자동 태깅) 안내
246
+ - Features 리스트에 "Knowledge Base (RAG)" 한 줄 추가
247
+
248
+ 코드 변경 없음 — docs only. 코드 표면은 3.17.0 과 동일.
249
+
250
+ ## [3.17.0] - 2026-05-18
251
+
252
+ ### Added — `cb.knowledge.addDocumentFromFile` (PDF / DOCX / text 파일 업로드)
253
+
254
+ `KnowledgeAPI.addDocumentFromFile(kbID, file, options?)` 를 신설했다. 브라우저 `<input type="file">`
255
+ 로 받은 `File` / `Blob` 또는 Node.js `Buffer` / `Uint8Array` 를 그대로 넘기면 SDK 가 base64 인코딩
256
+ + MIME 추출 후 서버에 보낸다. 서버는 PDF / DOCX / text 류를 자동 텍스트 추출하여 기존 청킹·인덱싱
257
+ 파이프라인을 태운다 (platform-issue `019e3a31`, ai-tool chat RAG 제보).
258
+
259
+ 지원 MIME:
260
+ - `application/pdf` (텍스트 PDF — 스캔 이미지 PDF 는 미지원)
261
+ - `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (DOCX)
262
+ - `text/*` (plain / markdown / csv / html)
263
+ - `application/json`
264
+
265
+ 파일 크기 상한: 50MB (원본 바이너리). 이미지 OCR / HWP / XLSX 는 향후 추가 예정 — 현재는 명시적 에러.
266
+
267
+ ```ts
268
+ // 브라우저
269
+ const file = event.target.files[0]
270
+ const doc = await cb.knowledge.addDocumentFromFile('kb-id', file, {
271
+ metadata: { tag: 'manual' },
272
+ })
273
+
274
+ // Node.js
275
+ import { readFileSync } from 'node:fs'
276
+ const doc = await cb.knowledge.addDocumentFromFile('kb-id', {
277
+ data: readFileSync('./report.pdf'),
278
+ mimeType: 'application/pdf',
279
+ name: 'report.pdf',
280
+ })
281
+ ```
282
+
283
+ `CreateDocumentRequest` 에 `file_content`(base64) / `mime_type` 옵션 필드를 추가해 기존 `addDocument`
284
+ 도 동일 흐름을 직접 호출 가능. `source_type` enum 에 `'file'` 추가.
285
+
286
+ 서버 측 변경 (core-server):
287
+ - `pkg/file_extractor` 신설 — PDF (ledongthuc/pdf, pure-Go) / DOCX (stdlib archive/zip + xml)
288
+ - `KnowledgeBaseService.createDocumentInternal` 가 source_type='file' 분기에서 base64 디코딩 →
289
+ 텍스트 추출 → 기존 청킹 파이프라인 진입
290
+ - MCP `add_kb_document` 도구도 `file_content` / `mime_type` 인자를 받아 SDK 와 동일한 표면 제공
291
+
292
+ 회귀 가드:
293
+ - `test/knowledge-add-document-from-file.test.ts` — 6 케이스 (Blob / File-like / Uint8Array / metadata /
294
+ default name / invalid input)
295
+ - backend `file_extractor` 10 케이스 + KB service `file_source_test.go` 5 케이스
296
+
297
+ 영향 범위:
298
+ - 신규 메서드 추가 — 기존 호출자 코드 변경 불필요.
299
+ - `CreateDocumentRequest` 가 두 optional 필드 추가만 — 기존 사용 패턴 호환.
300
+
301
+ ## [3.16.3] - 2026-05-18
302
+
303
+ ### Fixed — `persistence='none'` + OAuth redirect callback 첫 새로고침 시 세션 풀림
304
+
305
+ `new ConnectBase({ persistence: 'none' })` + `cb.oauth.signIn(provider, redirectUri)` (redirect 방식)
306
+ 로 로그인한 후 첫 새로고침에서 세션이 풀리는 회귀를 수정 (platform-issue `019e3960`, NJB 제보).
307
+
308
+ 원인: OAuth 콜백의 302 응답에 동봉된 `Set-Cookie: cb_member_refresh_token=...` 가 일부 deployment /
309
+ 브라우저 정책 / 사용자 설정 (3rd-party cookie 차단 등) 환경에서 브라우저에 저장되지 않는다.
310
+ `persistence='none'` 은 토큰을 메모리에만 두므로, 페이지 새로고침 후 SDK 가 `/v1/auth/re-issue` 를
311
+ cookie 없이 호출하게 되어 401 → `clearTokens` → 강제 로그아웃되었다.
312
+
313
+ 수정:
314
+ - `OAuthAPI.getCallbackResult()` / `exchangeCodeFromCallback()` 가 토큰을 메모리에 적재한 직후
315
+ `HttpClient.bootstrapRefreshCookie()` 를 fire-and-forget 으로 호출한다. 이 호출은
316
+ `persistence === 'none'` 일 때만 `/v1/auth/re-issue` 를 Bearer 흐름으로 한 번 실행해 서버가
317
+ HttpOnly cookie 를 명시적으로 발급하도록 유도한다.
318
+ - 백엔드 `/v1/auth/re-issue` 가 `Authorization: Bearer` + `X-Public-Key` 조합을 SDK 호출 신호로
319
+ 인식하면 응답에 `cb_member_refresh_token` cookie 를 함께 내려준다 (server-side 변경).
320
+
321
+ 회귀 가드:
322
+ - `test/oauth-callback-cookie-bootstrap.test.ts` — 4 케이스 (getCallbackResult / exchangeCodeFromCallback /
323
+ 에러 응답 / 토큰 누락)
324
+ - `test/http-cookie-bootstrap.test.ts` — 7 케이스 (persistence 별 동작, 비-브라우저, 5xx/401 silent 실패)
325
+
326
+ 영향 범위:
327
+ - `persistence='none'` (기본값) 으로 OAuth redirect callback 을 쓰던 모든 SDK 사용자.
328
+ `persistence='localStorage'` / `'sessionStorage'` 사용자는 변경 사항 없음.
329
+ - 추가 1회 re-issue 호출 비용 외에 호출자 코드 변경 불필요.
330
+
331
+ ## [3.16.2] - 2026-05-16
332
+
333
+ ### Fixed — OAuth redirect 콜백 페이지에서 autoRestoreSession ↔ getCallbackResult rotation race
334
+
335
+ OAuth 리다이렉트 콜백 페이지(`?access_token=&refresh_token=` legacy, `?code=&state=` code-only,
336
+ `?error=&state=` 에러) 에서는 SDK 생성자가 `autoRestoreSession` 을 더 이상 발화시키지 않는다.
337
+ 콜백 페이지에서 자동 복구가 발화하면 cookie 의 refresh token 이 즉시 rotation 되는데,
338
+ 곧이어 앱 코드가 `cb.oauth.getCallbackResult()` / `exchangeCodeFromCallback()` 으로 URL 의
339
+ *원본* refresh token 을 `setTokens()` 해버리면 메모리는 revoked 된 원본을 들고 있게 된다.
340
+ 이후 access 만료 시 SDK 가 Bearer 로 revoked token 을 보내 reuse-detection 에 걸려
341
+ 401 + family 전체 revoke 로 사용자가 강제 로그아웃되는 회귀가 있었다 (platform-issue
342
+ 2026-05-16, NJB).
343
+
344
+ 콜백 페이지에서는 `getCallbackResult()` / `exchangeCodeFromCallback()` 가 토큰 적재를
345
+ 책임지므로 자동 복구가 불필요하다. 일반 페이지(/, /play 등) 에서는 기존과 동일하게 발화한다.
346
+
347
+ 회귀 가드: `test/auto-restore-session-callback-skip.test.ts` — 6 케이스
348
+ (일반 / legacy 토큰-in-URL / code-only / error / 단독 access_token / autoRestoreSession=false).
349
+
350
+ ## [3.16.1] - 2026-05-16
351
+
352
+ ### Added — `SCRIPT_SETUP_OVERRUN` GameErrorCode literal
353
+
354
+ game-server 의 hook ctx 2-phase split (setup vs hook deadline 분리, backend
355
+ commit `ffe5108a` + `2132af4f`) 에 맞춰 `GameErrorCode` literal union 에
356
+ `'SCRIPT_SETUP_OVERRUN'` 을 추가했다. 사용자 hook 본문이 아닌 SDK 가 매 호출
357
+ 주입하는 setup 단계 (스크립트 로드 + state 직렬화) 가 budget 을 초과한 경우
358
+ 구분 가능하도록 한 4채널 동등 작업의 JS/TS 측. Unity / Godot / Unreal 도 동일
359
+ 시점에 동일 코드를 노출.
360
+
361
+ backward-compat: 신규 literal 추가만 — 기존 사용 패턴 호환.
362
+
363
+ ## [3.16.0] - 2026-05-14
364
+
365
+ ### Added — cross-app OAuth 로 Provider 함수 호출 / Database Realtime 구독
366
+
367
+ cross-app OAuth access token 으로 (1) Provider 앱의 ConnectBase Function 을 invoke 하고
368
+ (2) Provider 앱의 테이블을 Database Realtime 으로 구독하는 정식 경로를 추가했다.
369
+ 이전에는 cross-app OAuth 토큰이 보호 리소스에 도달하는 경로가 issue queue 뿐이라,
370
+ 발급받은 토큰으로 함수/Realtime 을 쓸 수 없었다 (platform issue `019e2645`).
371
+
372
+ - **`cb.functions.invokeCrossApp(providerAppId, functionId, accessToken, payload?, timeout?)`**
373
+ — Provider 가 `function:invoke` scope 를 노출하면 Consumer 가 cross-app OAuth access token
374
+ 으로 Provider 의 함수를 실행한다. 호출자 신원은 함수 런타임 `ctx.memberId`(end-user) /
375
+ `ctx.callerAppId`(Consumer 앱) 로 전달된다.
376
+ - **`cb.database.connectRealtime({ accessToken })`** — `accessToken` 이 cross-app OAuth
377
+ access token 이면 Provider 앱(`database:read` scope 노출 시)의 테이블을 구독한다. 구독
378
+ 대상 앱은 토큰 `aud` 로 결정되므로 Provider 의 publicKey 가 불필요하다. RLS 는 토큰
379
+ `end_user_id` 를 subject 로 평가한다 (member id 기반 규칙 정상 동작, email/role 기반
380
+ 규칙은 게스트로 fail-safe 평가).
381
+ - 함수 런타임 `ctx` 에 `memberId` / `callerAppId` 필드 추가 (nodejs/go/python/dotnet) —
382
+ cross-app 호출이 아니면 빈 문자열.
383
+
384
+ ## [3.15.0] - 2026-05-14
385
+
386
+ ### Added — `GameEventHandlers.onMessage` 커스텀 broadcast 메시지 핸들러
387
+
388
+ 게임 서버 Lua 의 `room.broadcast(data)` / `room.send_to(clientId, data)` 로 보낸,
389
+ SDK 가 모르는 `type` 의 커스텀 메시지를 받는 catch-all 핸들러를 추가했다.
390
+
391
+ 이전에는 `GameRoom.handleMessage` 의 `switch` 가 `delta`/`state`/`player_event`/
392
+ `chat`/`error` 표준 타입만 처리하고 `default:` 에서 메시지를 **drop** 했다. 그래서
393
+ 게임별 커스텀 프로토콜(예: `{ type: "chunk", ... }`, `{ type: "turn_played", ... }`)을
394
+ 클라이언트가 받을 방법이 없었다 — starter template 의 client 코드가 동작 불가능한
395
+ 상태였다.
396
+
397
+ - `room.on('onMessage', (msg) => { ... })` — 표준 타입(`delta`/`chat` 등)은 기존 전용
398
+ 핸들러로 가고, 그 외 커스텀 `type` 메시지만 `onMessage` 로 전달된다. `msg` 는
399
+ `{ type: string } & Record<string, unknown>` 형태이며 `msg.type` 으로 분기한다.
400
+ - 미설정 시 기존과 동일하게 무시(drop) — 하위 호환 100%, 동작 변화 없음.
401
+
402
+ `examples/game-prototypes/*/client/connect.ts` 의 3개 starter template 이 이 핸들러로
403
+ 재작성됐다 (`onMessage` 로 chunk / turn_played / character_moved 등 수신).
404
+
405
+ ## [3.14.2] - 2026-05-14
406
+
407
+ ### Fixed — `cb.game.createClient()` appId 누락 시 silent 연결 깨짐
408
+
409
+ NJB 측 `createRoom` → `SCRIPT_NOT_FOUND` 오탐 보고 (019e2210) 로 발견. `appId` 가
410
+ `new ConnectBase({ appId })` 에 설정되지 않은 상태에서 `cb.game.createClient()` 를 쓰면
411
+ `GameRoom.buildConnectionUrl` 이 `wss://.../v1/game//ws` (빈 path 세그먼트) 를 만들어
412
+ game-server 의 `conn.appID` 가 빈 문자열로 들어갔다. 그 결과 `createRoom` 의 스크립트
413
+ 검증이 `GetMeta("", name)` → `nats: invalid key` 로 실패해 실제로 존재하는 active
414
+ 스크립트가 `SCRIPT_NOT_FOUND` 로 오탐. HTTP 스크립트 메서드 (`uploadScript(appId, ...)`)
415
+ 는 appId 가 명시적 인자라 영향 없었고, WS 클라이언트만 전역 appId 에 의존해 깨졌음.
416
+
417
+ - `GameRoom.connect()` — `appId` 가 비어 있으면 빈 URL 로 붙는 대신 명확한 에러로 즉시
418
+ reject (`appId is required to connect ...`). `buildConnectionUrl` 에도 hard invariant
419
+ 가드 추가 — `/v1/game//ws` 는 더 이상 생성되지 않는다.
420
+ - `GameAPI.createClient({ appId })` — per-call `appId` 지정을 지원. 이전엔 `this.appId`
421
+ (전역) 가 `config.appId` 를 무조건 덮어써서 호출별 지정이 불가능했다. 이제
422
+ `config.appId ?? this.appId` 로 per-call 우선.
423
+
424
+ > game-server 측에도 defense-in-depth 보정 동반 — `public_key` / `token` 으로 인증된
425
+ > WS 연결은 auth 가 resolve 한 appID 를 authoritative 로 사용 (URL path 의 빈 `:appID`
426
+ > 무시). 유효한 publicKey 를 보내는 클라이언트는 SDK 업그레이드 없이도 자동 교정된다.
427
+
428
+ ### Added — 회귀 가드 테스트
429
+
430
+ - `test/game-connect-appid.test.ts` — appId 누락 시 `connect()` reject + WS 미생성,
431
+ `createClient({ appId })` per-call override, `/v1/game//ws` 절대 미생성 단언.
432
+
433
+ ## [3.14.1] - 2026-05-14
434
+
435
+ ### Fixed — `cb.game.*` primitive 호스트 라우팅 회귀 (v3.0.0 이후 잠복)
436
+
437
+ NJB 측 deleteScript / deactivateScript 404 보고 (2026-05-14) 로 발견. v3.0.0 BREAKING 에서
438
+ matchqueue / leaderboard / scripts primitive 메서드 16개가 `this.http.*` (기본 baseUrl
439
+ = core-server, `api.connectbase.world`) 를 거치도록 잘못 wired 되어 game-server
440
+ (`game.connectbase.world`) 로 라우팅되지 않고 404 회귀. NJB 는 v3.9.0 부터 `uploadScript`
441
+ 회귀를 raw fetch 로 우회 중이었음 (apps/game/scripts/upload-lua.mjs 참고).
442
+
443
+ 수정: `GameAPI` 내부 `gameFetch` private helper 도입. `${this.gameServerUrl}${path}` 기준
444
+ fetch + 표준 `getHeaders()` (X-Public-Key / Bearer) + 서버 응답 `{ error }` → `ApiError.code`
445
+ surface + 204/non-JSON 응답을 `undefined` 로 처리. 영향받은 메서드:
446
+
447
+ - **scripts (8)**: `uploadScript` / `listScripts` / `getScript` / `listScriptVersions` /
448
+ `activateScript` / `rollbackScript` / `deactivateScript` / `deleteScript`
449
+ - **matchqueue (3)**: `enqueueMatch` / `listMatchqueue` / `cancelMatch`
450
+ - **leaderboard (6)**: `submitScore` / `getTopScores` / `getMemberRank` /
451
+ `getRankAround` / `resetLeaderboard` / `removeFromLeaderboard`
452
+
453
+ 기존에 raw fetch 로 우회 중이던 통합 코드는 SDK 본 메서드로 회귀해도 무방 (4xx/5xx 시
454
+ `ApiError` 로 throw 되며 status/code 분기는 동일하게 작동).
455
+
456
+ ### Added — 회귀 가드 테스트
457
+
458
+ - `test/game-host-routing.test.ts` — 17개 메서드 전체가 `game.*` 호스트로 라우팅되는지
459
+ fetch mock 으로 단언. 동일 회귀 재발 방지.
460
+
461
+ ## [3.14.0] - 2026-05-14
462
+
463
+ ### Added — `GameError` 클래스 + `createRoom` 응답 메타
464
+
465
+ - 신규 export: `GameError` (extends `Error`) — game-server 의 `error` 메시지를 일관된
466
+ 인스턴스로 surface. `.code`, `.phase`, `.feature`, `.originClientId`, `.requested`,
467
+ `.available` 모두 노출. `instanceof GameError` 로 UI 분기 가능.
468
+ - 신규 export: `GameErrorCode` literal union — `'SCRIPT_NOT_FOUND' | 'NO_ACTION_HANDLER'
469
+ | 'FEATURE_DISABLED' | 'SCRIPT_ERROR' | 'SCRIPT_TIMEOUT' | 'RATE_LIMITED' |
470
+ 'QUOTA_EXCEEDED' | 'TIMEOUT' | 'UNKNOWN' | (string & {})`.
471
+ - 신규 export: `CreateRoomResult` 타입 — `{ roomId, state, scriptName?, scriptVersion? }`.
472
+ - 신규 메서드: `GameRoom.createRoomDetailed(config)` + `GameRoomTransport.createRoomDetailed(config)`
473
+ — server 가 attach 한 lua script 의 이름/버전을 client 가 즉시 검증할 수 있게 응답에 포함.
474
+ - 신규 getter: `room.scriptName` / `room.scriptVersion` (`GameRoom` + `GameRoomTransport`)
475
+ — 마지막 `createRoom` 응답의 메타. `joinRoom`/`leaveRoom`/`disconnect` 시 reset.
476
+ - 기존 `createRoom(config)` 의 시그니처는 그대로 (호환). 내부적으로 `createRoomDetailed`
477
+ 를 호출하고 `.state` 만 반환.
478
+ - `onError` 콜백 시그니처가 `Event | ErrorMessage` → `Event | GameError` 로 변경 (호환:
479
+ `ErrorMessage` 의 모든 필드가 `GameError` 인스턴스에 동일하게 surface 됨).
480
+
481
+ ### Changed (BREAKING) — `disableScript` rename + `deleteScript` 신규
482
+
483
+ server 측 `DELETE /v1/game/:appID/scripts/:name` 의 의미가 **Disable → hard-delete** 로
484
+ 변경된 것에 대응:
485
+
486
+ - `GameAPI.disableScript(appId, name)` **제거**. 대체: `GameAPI.deactivateScript(appId, name)`
487
+ — `POST /scripts/:name/deactivate` 호출 (코드/버전 보존, 재활성화 가능).
488
+ - `GameAPI.deleteScript(appId, name)` **신규** — `DELETE /scripts/:name` 호출 (메타 + 모든
489
+ 버전 영구 제거, 복구 불가).
490
+
491
+ ### Fixed — `error` 메시지 분류 메타 손실 회귀
492
+
493
+ 이전엔 모든 `reject(new Error(msg.data.message))` 로 server 의 `code`/`phase`/`feature`/
494
+ `origin_client_id`/`requested`/`available` 가 손실되어 SDK 사용자가 UI 분기를 만들 수
495
+ 없었다 (platform-issue 019e21dd, NJB 2026-05-13). 이제 `GameError` 인스턴스로 reject
496
+ 되어 모든 메타가 보존된다.
497
+
498
+ ### Migration
499
+
500
+ ```ts
501
+ // Before
502
+ import { ErrorMessage } from 'connectbase-client'
503
+ room.on('error', (err) => {
504
+ if ((err as ErrorMessage).code === 'FEATURE_DISABLED') { /* ... */ }
505
+ })
506
+ await cb.game.disableScript(appId, 'old-script')
507
+
508
+ // After
509
+ import { GameError } from 'connectbase-client'
510
+ room.on('error', (err) => {
511
+ if (err instanceof GameError) {
512
+ if (err.code === 'SCRIPT_NOT_FOUND') console.error('candidates:', err.available)
513
+ if (err.code === 'NO_ACTION_HANDLER') console.error('handler for', err.phase, 'undefined')
514
+ if (err.code === 'FEATURE_DISABLED') console.error('feature off:', err.feature)
515
+ if (err.originClientId) console.warn('error from other player:', err.originClientId)
516
+ }
517
+ })
518
+ await cb.game.deactivateScript(appId, 'old-script') // 비활성화 (코드 보존)
519
+ await cb.game.deleteScript(appId, 'really-old') // 영구 삭제 (신규)
520
+
521
+ // createRoom 응답 검증 패턴 (NJB regression 가드)
522
+ const { state, scriptName, scriptVersion } = await room.createRoomDetailed({ scriptName: 'njb-main' })
523
+ if (scriptName !== 'njb-main') {
524
+ throw new Error(`script attach mismatch: expected njb-main got ${scriptName}`)
525
+ }
526
+ console.log('attached', scriptName, 'v', scriptVersion)
527
+ ```
528
+
529
+ ## [3.13.1] - 2026-05-13
530
+
531
+ ### Documentation — README batch/transaction 예제 타입 회귀 수정
532
+
533
+ README 의 `cb.database.batch()` / `cb.database.transaction()` 예제가 구 시그니처
534
+ (`table: 'string'`) 를 사용해 `tsc --noEmit` 에서 `TS2353` 발생하던 문제 수정.
535
+
536
+ - `table:` → `table_id:` (UUID, 실제 `BatchOperation` 타입과 일치)
537
+ - 3.12+ 의 `success:false` throw 동작 반영 — try/catch 패턴 명시
538
+
539
+ 코드 변경은 없음. README 만 갱신.
540
+
541
+ ## [3.13.0] - 2026-05-13
542
+
543
+ ### Removed — `PlatformIssueDetail.triage_summary` / `triaged_at` 필드
544
+
545
+ ConnectBase 가 Platform Issue 의 AI 자동 분류 (triage) 기능을 통째 제거함에 따라
546
+ SDK 의 응답 타입에서도 다음 필드를 제거.
547
+
548
+ ```typescript
549
+ // 이전 (3.12 이하)
550
+ interface PlatformIssueDetail {
551
+ triage_summary?: string // ← 제거
552
+ triaged_at?: string // ← 제거
553
+ // ...
554
+ }
555
+
556
+ // 3.13+ — AI 요약 필드 없음. status / resolution_note / external_links 로 진행 확인.
557
+ ```
558
+
559
+ `status='triaged'` enum 값은 운영자 수동 분류 의미로 유지된다 (코드 변경 불필요).
560
+
561
+ ### Behavior change — `cb.support.reportPlatformBug` 의 category/severity 자동 보정 없음
562
+
563
+ 이전: AI triage 가 발행 직후 비동기로 category / severity 를 보정.
564
+ 3.13+: 운영자가 admin 콘솔에서 수동 조정. SDK 발행자가 지정한 값이 그대로 유지된다.
565
+
566
+ ### Backend wiring (참고)
567
+
568
+ - `backend/cmd/core-server/app/worker/platform_issue_triage/` 워커 전체 제거
569
+ - `ent` 컬럼 6개 (`triage_summary`/`triage_keywords`/`triage_similar_issue_ids`/`body_embedding`/`triaged_at`/`triage_skip_reason`) 드롭
570
+ - `POST /v1/admin/platform-issues/:id/retriage` + `/bulk-retriage` endpoint 제거
571
+ - MCP `admin_retriage_platform_issue` 도구 제거
572
+
573
+ ## [3.12.0] - 2026-05-13
574
+
575
+ ### Fixed — `cb.database.batch()` / `cb.database.transaction()` silent success 회귀 (platform-issue 019e1c9c)
576
+
577
+ 서버가 `success: false` + 개별 op `error` 로 부분 실패를 표현해도 SDK 가 그냥 Promise 를 resolve 해
578
+ 호출자가 "성공" 으로 오해하던 문제 수정. 이제 첫 실패 op 의 error 메시지로 `throw`.
579
+
580
+ ```typescript
581
+ try {
582
+ await cb.database.batch([{
583
+ type: 'update',
584
+ table_id: '019d86bf-...',
585
+ doc_id: '019de33a-...',
586
+ operators: { like_count: { type: 'increment', value: 1 } },
587
+ }])
588
+ } catch (e) {
589
+ // 이제 RLS 거부, table_id 오타, 검증 실패 등이 명확히 throw 됨
590
+ console.error(e.message)
591
+ }
592
+ ```
593
+
594
+ ### Added — `BatchWriteResult` / `TransactionResult` / `BatchOperationResult` / `TransactionWriteResult` 타입 export
595
+
596
+ batch/transaction 응답이 `{ results: Record<string, unknown>[] }` 에서 정형 타입으로 변경:
597
+
598
+ ```typescript
599
+ import type { BatchWriteResult, BatchOperationResult } from 'connectbase-client'
600
+
601
+ const r: BatchWriteResult = await cb.database.batch(ops)
602
+ // r.success: boolean
603
+ // r.results[i]: { index, success, doc_id?, error? }
604
+ // r.total_count / success_count / failed_count
605
+ ```
606
+
607
+ ### Behavior change — batch/transaction 부분 실패가 더 이상 silent 통과되지 않음
608
+
609
+ 이전: `success: false` 응답도 `await cb.database.batch(...)` 가 정상 resolve → 호출자가 매번
610
+ `response.success` 와 `response.results[i].success` 를 직접 확인해야 했음.
611
+
612
+ 본 버전부터: `success: false` 면 즉시 `throw new Error(<첫 실패 op error>)`. 부분 실패를
613
+ silent 처리하던 코드가 있다면 try/catch 로 감싸야 한다.
614
+
615
+ ### Backend wiring (참고)
616
+
617
+ 본 fix 는 data-server 의 다음 변경과 짝을 이룬다 (별도 배포 필요):
618
+ - `transaction_service` 의 3 callsite (executeRead / RunTransaction commit / BatchWrite) 가
619
+ `table_id` (UUID) 를 title 로 잘못 lookup → 항상 "ent: table not found" 회귀를 함께 수정
620
+ (platform-issue 019e1c9c/eba6).
621
+ - batch/transaction 이 RLS 평가를 통째로 건너뛰던 보안 회귀 수정. operators 가 머지된 finalData
622
+ 를 newData 로 RLS 에 전달해 `.update` predicate 가 실제 변경 결과 기준으로 평가됨
623
+ (platform-issue 019e1c9c/c155).
624
+
625
+ ## [3.11.0] - 2026-05-10
626
+
627
+ ### Fixed — `cb.game.createClient().createRoom()` 의 `scriptName` / 기타 wire 필드 매핑 (platform-issue 019e123a)
628
+
629
+ 게임 룸 생성 시 SDK 의 camelCase config (`scriptName`, `tickRate`, `maxPlayers`, `roomId`,
630
+ `categoryId`) 가 게임 서버 (Go 핸들러 JSON 태그 = snake_case) 가 기대하는 와이어 형식으로
631
+ 매핑되지 않아, **`scriptName` 미전달 → `RoomConfig.ScriptID` 비어 있음 → `onTick` /
632
+ `onPlayerJoin` 등 모든 사용자 Lua hook 이 silent skip** 되던 문제 수정. NJB 앱 보고로 확인.
633
+
634
+ ### Added — `GameRoomConfig.scriptName`
635
+
636
+ ```typescript
637
+ const room = cb.game.createClient({ clientId: 'p1' })
638
+ await room.connect()
639
+ await room.createRoom({
640
+ tickRate: 20,
641
+ maxPlayers: 100,
642
+ scriptName: 'njb-main', // ← 콘솔/REST 로 업로드+활성화한 Lua 스크립트 이름
643
+ })
644
+ ```
645
+
646
+ 지정 시 룸의 `onRoomCreate` / `onPlayerJoin` / `onTick` / `onAction` / `onLeave` 가 해당
647
+ 스크립트로 디스패치된다. 미지정 시 server tick + delta 만 흐르고 사용자 hook 은 호출되지 않음.
648
+
649
+ ### Added — `toCreateRoomWire(config)` 헬퍼 export
650
+
651
+ 테스트/디버깅 용으로 `import { toCreateRoomWire } from 'connectbase-client'`. SDK 가 내부적으로
652
+ `create_room` WebSocket 메시지 생성 시 사용하는 매핑 함수.
653
+
654
+ ### Behavior change — `tickRate` / `maxPlayers` 등이 이제 실제로 적용됨
655
+
656
+ 이전 버전은 camelCase 그대로 와이어로 보냈으나 게임 서버가 snake_case 만 인식해 모든 룸이
657
+ **서버 기본값 (tick_rate=64, max_players=100)** 으로 생성됐다. 본 버전부터 SDK 가 명시한 값이
658
+ 실제로 반영된다. 이전 동작에 의존한 코드가 있다면 명시 값을 검토할 것.
659
+
660
+ ### Backend wiring (참고)
661
+
662
+ 본 fix 는 game-server 의 다음 변경과 짝을 이룬다 (별도 배포 필요):
663
+ - `CreateRoomRequest` 에 `script_name` (+ `script` alias) 필드 추가, `handleCreateRoom` 이
664
+ `RoomConfig.ScriptID = appID:scriptName` 으로 매핑
665
+ - 신규 `scripts.Loader` — 게임 서버 부팅 시 활성 스크립트 일괄 로드 + `game.scripts.reload.>`
666
+ NATS 구독으로 Activate/Rollback/Disable 이벤트를 ScriptEngine 에 반영. legacy `HotReloader`
667
+ 는 다른 subject/schema 라 미사용 코드였음 (이번에 발견된 누락)
668
+
669
+ ## [3.10.0] - 2026-05-10
670
+
671
+ ### Fixed — `/v1/auth/re-issue` 일시 실패에 강제 로그아웃 (platform-issue 019e11cf)
672
+
673
+ `HttpClient.refreshAccessToken()` 가 모든 실패에 즉시 토큰을 폐기해, 5xx / 네트워크 hiccup
674
+ 한 번에 활동 중인 사용자가 강제 로그아웃되던 문제를 수정. OAuth 클라이언트 표준
675
+ (Auth0 SPA SDK / MSAL / Amplify) 에 맞춰 실패를 분류한다.
676
+
677
+ | 응답 | 분류 | 토큰 처리 | 호출되는 콜백 |
678
+ |---|---|---|---|
679
+ | `5xx` / 네트워크 / abort / 손상된 200 | transient | **보존** (다음 호출에서 backoff 후 재시도) | `onTransientRefreshFailure`, `onAuthError` |
680
+ | `401` / `403` / `400 invalid_grant` / `400 invalid_token` | permanent | 폐기 | `onTokenExpired`, `onAuthError` |
681
+ | 그 외 4xx (예: `400 invalid_request`) | client_bug | **보존** (재시도 무의미, 패치 배포로 회복) | `onAuthError` |
682
+
683
+ ### Added — `onTransientRefreshFailure` 콜백
684
+
685
+ 일시적 refresh 실패 시 호출되는 새 콜백. 토큰은 살아있으므로 강제 로그아웃 없이
686
+ "연결이 잠시 불안정합니다" 같은 비파괴 알림을 띄울 때 사용:
687
+
688
+ ```typescript
689
+ const cb = new ConnectBase({
690
+ publicKey: '...',
691
+ onTransientRefreshFailure: () =>
692
+ toast.warn('연결이 잠시 불안정합니다. 잠시 후 자동 복구됩니다.'),
693
+ onTokenExpired: () => { window.location.href = '/login' },
694
+ })
695
+ ```
696
+
697
+ ### Behavior change (semver minor 로 분류한 이유)
698
+
699
+ `onTokenExpired` 의 호출 시점이 **refresh token 자체가 무효화된 경우로 한정**된다.
700
+ 이전 버전에서는 transient 실패에도 호출됐기에, 그 콜백을 "임의 refresh 실패 알림" 으로
701
+ 사용하던 앱은 동작 변화가 있다. transient 알림은 새 `onTransientRefreshFailure` 로 분리.
702
+ `onAuthError` 는 모든 실패에서 호출되는 점은 동일.
703
+
704
+ ## [3.9.0] - 2026-05-10
705
+
706
+ ### Added — Support API (end-user issue reporting)
707
+
708
+ 앱 사용자가 운영자에게 버그·질문·요청을 발행하는 채널을 SDK 메서드로 노출.
709
+
710
+ ```typescript
711
+ await cb.support.reportIssue({
712
+ title: "결제 화면이 멈춰요",
713
+ body: "결제 버튼 클릭 후 로딩이 끝나지 않습니다.",
714
+ category: "bug",
715
+ metadata: { pageUrl: window.location.href },
716
+ // 익명 발행
717
+ anonymousEmail: "user@example.com",
718
+ recaptchaToken: await grecaptcha.execute(SITE_KEY, { action: 'report_issue' }),
719
+ })
720
+ ```
721
+
722
+ - **AppMember JWT 자동 첨부**: 로그인 사용자는 `reporter_member_id` 자동 채워짐
723
+ - **익명 발행 지원**: AppMember 없어도 발행 가능 (운영자가 reCAPTCHA 활성화한 경우 토큰 권장)
724
+ - **응답 최소화**: `{ id, status: 'open', created_at }` 만 — 봇이 ID 구조 학습 회피
725
+ - **AI 자동 triage**: 백엔드(core-server)가 발행 직후 비동기로 요약·긴급도·카테고리·키워드·유사이슈 자동 분류
726
+ - **카테고리**: `bug` | `question` | `feature_request` | `incident` | `other`
727
+
728
+ 서버 측 가이드: [docs/cross-app-issue.md](https://github.com/connectbase-world/connectbase/blob/release/docs/cross-app-issue.md), 외부 ticketing 라우팅: [docs/cross-app-issue-webhook-guide.md](https://github.com/connectbase-world/connectbase/blob/release/docs/cross-app-issue-webhook-guide.md).
729
+
730
+ ## [3.8.1] - 2026-05-07
731
+
732
+ ### Fixed — `/v1/auth/re-issue` 가 콘솔/SDK cookie 공존 시 잘못된 토큰 발급
733
+
734
+ 3.8.0 의 cookie 우선순위가 [member → user → Bearer] 로 고정되어, 같은 브라우저에
735
+ platform 콘솔용 `refresh_token` 과 SDK 용 `cb_member_refresh_token` 이 공존할 때
736
+ (예: 외부 앱 SDK 로그인 후 콘솔 로그인) 콘솔의 `/v1/auth/re-issue` 호출이
737
+ AppMember access token 을 받아 콘솔 측 endpoint(예: `/v1/cli-auth/approve/:id`,
738
+ `claims.UserID == nil`) 에서 401 발생하던 회귀를 수정했습니다.
739
+
740
+ - SDK 의 `refreshAccessToken()` 이 `/v1/auth/re-issue` 호출 시 `X-Public-Key` 헤더를
741
+ 첨부합니다. 백엔드는 이 헤더로 SDK 호출(member cookie 우선) vs 콘솔 호출
742
+ (user cookie 우선) 을 식별합니다.
743
+ - 백엔드 `auth_controller.ReIssueAccessToken` 우선순위:
744
+ 1. `Authorization: Bearer` (가장 명시적)
745
+ 2. cookie 단독 시 X-Public-Key 헤더 유무로 분기
746
+ - Bearer 흐름 / 단일 cookie 흐름은 영향 없음. 회귀 시나리오만 정상화.
747
+
748
+ 3.8.0 사용자는 3.8.1 로 업데이트하면 됩니다. 백엔드 측은 함께 배포 필요.
749
+
750
+ ## [3.8.0] - 2026-05-07
751
+
752
+ ### Added — HttpOnly cookie 기반 refresh token 흐름 (XSS 면역 default 세션)
753
+
754
+ 3.7.x 의 `persistence` 콘솔 경고가 권고하던 "HttpOnly cookie + 기본값('none')" 흐름을
755
+ 실제로 동작하도록 SDK + 백엔드를 함께 구현했습니다.
756
+
757
+ - `persistence: 'none'` (기본값) 으로도 **새로고침 후 자동 복구** 가능. refresh token 은
758
+ 서버 HttpOnly cookie 로만 보관되어 JS 가 접근할 수 없습니다 (XSS 시 탈취 불가).
759
+ - `localStorage` / `sessionStorage` 옵션은 여전히 사용 가능하지만 위험 경고가 유지됩니다.
760
+
761
+ **SDK 변경:**
762
+
763
+ - 모든 ConnectBase API fetch 호출에 `credentials: 'include'` 적용 — HttpOnly refresh cookie 가
764
+ 자동 첨부됩니다 (`api.connectbase.world` host-only cookie 기준).
765
+ - `/v1/auth/re-issue` 호출이 cookie 만으로 동작 — 메모리에 refresh token 이 없어도 cookie 가
766
+ 있으면 access token 회복.
767
+ - `ConnectBase` 옵션에 `autoRestoreSession?: boolean` 추가 (브라우저 기본 true). 인스턴스 생성
768
+ 시 자동으로 cookie 기반 세션 복구를 시도하며, 미로그인/cookie 만료 시 silent 실패.
769
+ - `cb.restoreSession(): Promise<boolean>` 메서드로 명시적 await 도 가능.
770
+ - `persistence` 콘솔 경고를 갱신: 위험은 그대로 표시하되 'none' + HttpOnly cookie 흐름이
771
+ 실제로 작동함을 안내.
772
+
773
+ **백엔드 변경 (core-server):**
774
+
775
+ - `pkg/util/cookie` 에 `CrossSite` / `HostOnly` 옵션 추가 (SameSite=None + Secure + host-only).
776
+ - 새 공용 헬퍼 `core-server/app/util/auth_cookie/` — platform 용 `refresh_token` 과 AppMember/OAuth
777
+ 용 `cb_member_refresh_token` 두 종류를 분리해 충돌 방지.
778
+ - `/v1/public/app-members/{signin,signup,signout}` + `CreateGuestMember` + OAuth callback/exchange
779
+ 엔드포인트가 refresh token 을 HttpOnly cookie 로 발급.
780
+ - `/v1/auth/re-issue` 가 입력 우선순위 `[member cookie → user cookie → Authorization Bearer]`
781
+ 로 처리하며, cookie 흐름은 sliding (재호출 시 cookie 만료 7일 연장).
782
+ - 응답 body 의 `refresh_token` 은 하위호환을 위해 그대로 유지 (구버전 SDK / Node.js / 게임 SDK
783
+ 호환). 신규 SDK 는 cookie 만 신뢰합니다.
784
+
785
+ **마이그레이션:**
786
+
787
+ 기존 `persistence: 'sessionStorage'` 또는 `'localStorage'` 사용 중이었다면 옵션을 제거하기만
788
+ 하면 됩니다 (default 가 안전 흐름):
789
+
790
+ ```ts
791
+ // before (3.7.x)
792
+ new ConnectBase({ publicKey, persistence: 'sessionStorage' })
793
+
794
+ // after (3.8.0)
795
+ new ConnectBase({ publicKey }) // persistence: 'none' + autoRestoreSession: true (기본)
796
+ ```
797
+
798
+ 다른 origin 에서 호출하는 경우 (앱 도메인 ≠ `api.connectbase.world`) CORS 화이트리스트에 등록되어
799
+ 있어야 합니다 — 콘솔의 커스텀 도메인 등록 흐름이 그대로 적용됩니다.
800
+
801
+ ## [3.7.2] - 2026-05-01
802
+
803
+ ### Fixed — `cb.endpoint.connectWebSocket()` 메시지 깨짐 + permessage-deflate 누설
804
+
805
+ 3.7.0/3.7.1 의 endpoint WS pass-through 가 handshake 는 101 정상이지만 모든
806
+ 메시지가 binary frame 으로 도착하고 payload 가 origin (ComfyUI/aiohttp) 의
807
+ raw WS frame bytes (RSV1=1 압축 frame 포함) 라 client 의 native WebSocket
808
+ 이 디코드 불가하던 문제를 수정했습니다. 두 개의 별개 버그가 합쳐진 회귀:
809
+
810
+ 1. **CLI 가 WS frame parse/encode 안 함** — 기존 `startWSStream` 이
811
+ `socket.on('data')` 의 raw bytes 를 그대로 v2 binary frame payload 로
812
+ forward 해서 client 에 frame header (0x81/0x82/0xc1) 가 섞인 상태로 도달.
813
+ 2. **`Sec-WebSocket-Extensions` 가 upstream 까지 forward 됨** — 브라우저 native
814
+ WebSocket 이 default 로 `permessage-deflate` 를 광고하고, sanitize 가 hop-by-hop
815
+ 만 strip 해서 upstream (aiohttp default) 이 accept → compressed frame 송신
816
+ 시작. CLI 는 deflate context 가 없어 디코드 불가.
817
+
818
+ **Fix (CLI):**
819
+
820
+ - `UpstreamWsFrameParser`: incoming WS frame 을 parse 해서 payload 만 추출.
821
+ TEXT/BINARY/CONTINUATION/PING/CLOSE 처리, fragmented frame 은 누적, RSV1
822
+ (compression) frame 은 protocol error 로 거부.
823
+ - `buildClientFrame` / `createUpstreamTextFrame`: outgoing payload 를 RFC 6455
824
+ §5.3 client masking 적용한 masked WS frame 으로 encode 후 upstream 에 write.
825
+ - 로컬 upstream 요청에서 `Sec-WebSocket-Extensions` 헤더 strip — proxy chain 이
826
+ deflate context 를 end-to-end 로 carry 하지 못하므로 compression 자체를 비활성.
827
+
828
+ **회귀 영향 범위:** 3.7.0 의 모든 `cb.endpoint.connectWebSocket()` 사용자.
829
+ ComfyUI / vLLM 등 default-on compression origin 영향. 사용자가 3.7.2 로 업그레이드
830
+ + 터널 재시작 시 즉시 정상화. text/binary 구분은 client 측에서 항상 binary
831
+ (ArrayBuffer) 로 도달 — JSON 은 `TextDecoder.decode` 로 string 변환 필요.
832
+ Opcode propagation 은 별도 follow-up.
833
+
834
+ **회귀 가드:** `UpstreamWsFrameParser` 단위 테스트 (TEXT payload 추출 / fragmented
835
+ 누적 / RSV1 reject / TCP coalescing / split-across-chunks / PING callback) +
836
+ `buildClientFrame` masking round-trip + `WSStreamForwarder` e2e (real local HTTP
837
+ server 로 `Sec-WebSocket-Extensions` 미도달 검증).
838
+
839
+ ## [3.7.1] - 2026-05-01
840
+
841
+ ### Fixed — `cb.endpoint.connectWebSocket()` 가 502 로 떨어지던 회귀
842
+
843
+ 3.7.0 의 endpoint WS pass-through 가 client 단에서 항상 CF 502 page 로 떨어지던
844
+ 문제를 수정했습니다. Root cause 는 CLI 가 로컬 upstream HTTP 요청에 WebSocket
845
+ upgrade 헤더 (`Connection: Upgrade` + `Upgrade: websocket`) 를 누락한 것:
846
+ upstream (ComfyUI 등) 이 일반 GET 으로 인식하고 400 Bad Request 반환 → core-server
847
+ ReverseProxy 가 비-101 body 를 relay 하려다 `net/http: abort Handler` panic →
848
+ CF 가 corrupted response 를 자체 502 page 로 substitute.
849
+
850
+ **Primary fix (CLI):**
851
+
852
+ - `cli.ts` `startWSStream` 과 `tunnel-v2.ts` `WSStreamForwarder` 가 upstream
853
+ 요청에 `Connection: Upgrade` + `Upgrade: websocket` 명시적으로 set. tunnel-server
854
+ 의 `sanitizeRequestHeaders` 가 RFC 7230 hop-by-hop 으로 strip 하므로 CLI →
855
+ upstream 새 hop 에서 다시 추가 필요.
856
+
857
+ **Defensive fix (core-server, 자동 deploy):**
858
+
859
+ - `ForwardWebSocket` `ModifyResponse` 에서 비-101 upstream 응답 시 ErrorHandler
860
+ 경로로 라우팅 — ReverseProxy 가 비-101 body 를 relay 하려다 panic 하는 path
861
+ 차단. Upstream 이 정당한 사유로 비-101 (rate limit / auth 실패 등) 반환할 때도
862
+ 깨끗한 502 + 진단 메시지가 client 까지 도달.
863
+
864
+ **회귀 영향 범위:** 3.7.0 의 모든 `cb.endpoint.connectWebSocket()` 사용자.
865
+ 사용자가 3.7.1 로 업그레이드 + 터널 재시작 시 정상 동작.
866
+
867
+ **회귀 가드:** `tunnel-v2.test.ts` 가 `WSStreamForwarder` 의 upgrade 헤더 송신을
868
+ real local HTTP server 로 e2e 검증. Go 측 `proxy_service_test.go
869
+ TestForwardWebSocket_Non101UpstreamReturnsCleanError` 가 panic 없는 깨끗한 502
870
+ 반환을 락인.
871
+
872
+ ## [3.7.0] - 2026-05-01
873
+
874
+ ### Added — `cb.endpoint.connectWebSocket()` 네이티브 WebSocket 지원
875
+
876
+ Endpoint Proxy v2 (Phase 5) 가 출하되어, SDK 사용자가 `cb.endpoint.connectWebSocket()`
877
+ 한 줄로 ComfyUI / vLLM / 일반 모델 서버의 WebSocket 엔드포인트를 직접 연결할 수
878
+ 있습니다. CLI 도 v2 endpoint (`/v2/tunnel/connect`) 를 사용하도록 전환.
879
+
880
+ > **요구사항:** 백엔드 v2 endpoint 가 먼저 배포돼 있어야 동작합니다. 프로덕션
881
+ > 배포 완료 후 SDK 업그레이드를 권장합니다.
882
+
883
+ > **알려진 이슈:** 3.7.0 / 3.7.1 에서 WS frame leakage / 502 회귀가 발견되어
884
+ > **3.7.2 이상 사용 권장**. 자세한 내용은 3.7.1 / 3.7.2 항목 참고.
885
+
886
+ ## [3.6.0] - 2026-05-01
887
+
888
+ ### Added — `cb.realtime.stream()` 멀티모달 메시지 (Vision) 지원
889
+
890
+ `cb.realtime.stream()` 의 `content` 필드가 `string` 외에도 OpenAI Vision spec
891
+ 호환 array 형식 (`{ type: 'text', text }` 또는 `{ type: 'image_url', image_url: { url } }`
892
+ 파트 배열) 을 받을 수 있게 됐습니다. 이미지 첨부 채팅을 SDK 한 줄로 작성 가능.
893
+
894
+ **예시:**
895
+
896
+ ```ts
897
+ await cb.realtime.stream(
898
+ [{
899
+ role: 'user',
900
+ content: [
901
+ { type: 'text', text: '이 이미지에 보이는 동물은?' },
902
+ { type: 'image_url', image_url: { url: 'https://.../cat.jpg' } },
903
+ ],
904
+ }],
905
+ { onToken, onDone, onError },
906
+ { provider: 'openai_compatible' },
907
+ )
908
+ ```
909
+
910
+ **Provider 별 처리:**
911
+
912
+ - **`openai` / `openai_compatible` (vLLM, LM Studio, Ollama 등)**: content array 를
913
+ 그대로 passthrough — vLLM 의 Qwen-VL / LLaVA / InternVL 등 모든 OpenAI Vision
914
+ 호환 비전 모델 즉시 동작.
915
+ - **`claude`**: `data:image/...;base64,...` URI 는 Claude 의 `source.type=base64`
916
+ 콘텐츠 블록으로 변환, `https://...` URL 은 `source.type=url` 로 위임 (Anthropic
917
+ API 가 fetch 담당). 텍스트 파트는 `type=text` 블록으로 변환.
918
+ - **`gemini`**: Gemini API 가 외부 URL fetch 를 지원하지 않으므로 SDK 가 사전
919
+ fetch (timeout 30s, 20MB 사이즈 캡, image/* MIME 검증) → base64 `inline_data`
920
+ 로 변환. data URI 는 즉시 분해.
921
+
922
+ **SDK 타입 변경:**
923
+
924
+ - `StreamMessage.content`: `string` → `string | StreamContentPart[]` (union, 하위 호환).
925
+ - 신규 export: `StreamContentPart`, `StreamTextPart`, `StreamImageURLPart`.
926
+
927
+ **서버 변경:**
928
+
929
+ - socket-server `protocol.StreamMessage.Content` 를 `json.RawMessage` 로 변경 후
930
+ 새 helper `protocol.ParseStreamContent()` 가 string/array 를 통일 디코드.
931
+ - pkg/ai 의 `Message` 에 `Parts []ContentPart` 추가 — provider 별 wire format 으로
932
+ 분기 직렬화. `Message.Content` (string) 는 기존과 동일하게 유지 → 모든 string-only
933
+ 호출자 (core-server, mcp-server 등) 회귀 무영향.
934
+
935
+ **회귀 가드:** 11 multimodal provider 직렬화 테스트 + 11 protocol/handler 검증
936
+ 테스트 + 3 convertMessages plumbing 테스트 추가.
937
+
938
+ ## [3.5.3] - 2026-05-01
939
+
940
+ ### Fixed — `connectbase tunnel --label` 의 `tunnel_id` 추출 실패
941
+
942
+ 3.5.2 의 `parseArgs()` 패치로 `--label` / `--description` 이 정상 파싱되어
943
+ `registerEndpointBinding()` 까지 호출되는 것은 확인됐으나, 함수 진입 직후
944
+ `tunnel_id` 추출 단계에서 즉시 skip 되어 자동 등록이 여전히 한 번도
945
+ 수행되지 않던 문제를 수정했습니다.
946
+
947
+ 원인: 추출 로직이 `https://<id>.tunnel.connectbase.world` (subdomain 기반)
948
+ 형식을 가정했으나, 실제 발급 URL 은 `https://tunnel.connectbase.world/<id>`
949
+ (path 기반) 입니다. `host.replace(/\.tunnel\.connectbase\.world$/, '')` 가
950
+ 정규식 매칭에 실패해 입력을 그대로 반환 → `tunnelId === host` 조건으로
951
+ "tunnel_id 추출 실패" 분기 진입 → 자동 등록 skip.
952
+
953
+ - URL 파싱을 제거하고, 서버가 `tunnel_ready` 메시지에 직접 실어 보내는
954
+ `tunnel_id` 필드를 그대로 사용하도록 변경 (서버 권위값 기준 — 향후 URL
955
+ 형식 변경에 영향받지 않음).
956
+ - `registerEndpointBinding()` 시그니처: `tunnelUrl` → `tunnelId` 로 단순화 +
957
+ `export` 노출 → 단위 테스트 가능.
958
+ - `test/cli-register-endpoint.test.ts` 회귀 가드 5 케이스 신규 추가:
959
+ 201 (정상 등록 — body.tunnel_id 검증), description 기본값, baseUrl trailing
960
+ slash 정규화, appId URL 인코딩, 409/401/5xx/네트워크 오류 시 호출부
961
+ 흐름 유지 (throw 하지 않음).
962
+
963
+ 회귀 영향 범위: 3.5.2 의 `--label` 자동 등록 사용자 (해당 기능을 처음으로
964
+ 정상 동작시킴). 다른 옵션 / 기본 `tunnel` 동작에는 영향 없음.
965
+
966
+ ## [3.5.2] - 2026-05-01
967
+
968
+ ### Fixed — `connectbase tunnel --label` / `--description` 옵션이 파싱되지 않던 문제
969
+
970
+ 3.5.1 의 인증 헤더 / `await` 수정에도 불구하고 `--label` / `--description` 으로
971
+ endpoint binding 자동 등록이 한 번도 트리거되지 않던 문제를 수정했습니다.
972
+ 원인은 `parseArgs()` 의 옵션 파싱 분기 자체가 두 옵션을 매칭하지 않아
973
+ `parsed.options.label === undefined` 로 떨어지고, 결과적으로
974
+ `if (tunnelOpts?.label && tunnelUrl)` 가드가 항상 false 가 되어
975
+ `registerEndpointBinding()` 이 호출되지 않았던 것입니다.
976
+
977
+ - `parseArgs()` 의 while 루프에 `--label` / `--description` 분기 추가
978
+ (기존 `--storage` / `--max-body` 와 동일 패턴).
979
+ - `parseArgs()` 를 `export` 로 노출 + `test/cli-parse-args.test.ts` 회귀 테스트
980
+ 추가 — `--label` 단독 / `--label` + `--description` 조합 / 미지정 / `--description`
981
+ 단독 4 가지 케이스를 검증해 동일 회귀 차단.
982
+
983
+ 회귀 영향 범위: 3.5.1 에서 `--label` / `--description` 을 사용했던 모든 호출
984
+ (즉 endpoint binding 자동 등록 기능 전체). 다른 옵션·기본 `tunnel` 동작에는 영향 없음.
985
+
986
+ ## [3.5.1] - 2026-05-01
987
+
988
+ ### Fixed — `connectbase tunnel --label` endpoint 자동 등록 401
989
+
990
+ `--label` 옵션 사용 시 호출되는 `POST /v1/apps/:appID/endpoints/cli` 가 모든 요청에서
991
+ 401 ("유효하지 않은 토큰입니다") 으로 떨어져 endpoint binding 이 한 번도 등록되지
992
+ 않던 문제를 수정했습니다. 결과적으로 `cb.endpoint.call("label", ...)` 가 항상
993
+ `endpoint not found` (404) 로 실패했습니다.
994
+
995
+ - CLI 가 `Authorization: Bearer cb_sk_*` 로 호출하던 것을 서버 dual-auth 미들웨어가
996
+ 요구하는 **`X-Public-Key: cb_sk_*`** 헤더로 변경.
997
+ - `void registerEndpointBinding(...)` 의 fire-and-forget 호출을 `await` 로 변경 —
998
+ 성공/실패 메시지가 "Ctrl+C 로 종료" 안내 앞에 출력됨.
999
+ - 등록 실패 / 네트워크 오류는 stdout 이 아닌 **stderr** (빨간색 ✗) 로 출력 —
1000
+ 파이프/리다이렉트 환경에서도 사용자가 실패를 인지할 수 있도록.
1001
+
1002
+ 회귀 영향 범위: `--label` 사용자만 해당 (기본 `connectbase tunnel <port>` 는 영향 없음).
1003
+
1004
+ ## [3.5.0] - 2026-04-30
1005
+
1006
+ ### Added — Knowledge Base 사용자별 격리
1007
+
1008
+ 다중 사용자 SaaS RAG 챗봇 시나리오에서 한 KB 안의 문서를 사용자 단위로 격리할 수
1009
+ 있도록 검색 요청에 `where` 필드 + magic 토큰을 추가했습니다. **모든 격리 enforcement
1010
+ 는 서버측에서 수행** 되므로 클라이언트 변조 불가.
1011
+
1012
+ - **`KnowledgeSearchRequest.where?: Record<string, unknown>`** — metadata 기반 필터.
1013
+ 키 형식 `metadata.<path>` 또는 raw key. 검색 시 청크 metadata JSON 의 path 매칭.
1014
+ - **`AUTH_MEMBER_ID_TOKEN`** (`'$auth.member_id'`) — magic 토큰 export. `where` 값에
1015
+ 사용하면 서버가 인증된 AppMember ID 로 자동 치환. AppMember JWT 가 함께 오지 않은
1016
+ 호출에서 사용 시 401.
1017
+
1018
+ ### Behavior — Authorization 헤더로 격리 활성화
1019
+
1020
+ `Authorization: Bearer <appmember-jwt>` 헤더가 함께 오면 서버가 자동으로:
1021
+
1022
+ 1. 검색 결과를 본인 `metadata.user_id` 문서로 한정
1023
+ 2. `addDocument` 시 `metadata.user_id` 자동 태깅 (클라이언트가 다른 값을 넣어도 강제 덮어씀)
1024
+ 3. `listDocuments` 가 본인 자료만 반환
1025
+ 4. `deleteDocument` 가 본인 자료만 허용 (cross-user → 403)
1026
+ 5. AI chat 의 RAG 검색 (`cb.ai.chatStream({ knowledgeBaseId })`) 도 동일 격리
1027
+
1028
+ 헤더가 없으면 기존 동작 그대로 — 앱 단위 공유. **하위 호환 보장**.
1029
+
1030
+ ```ts
1031
+ import ConnectBase, { AUTH_MEMBER_ID_TOKEN } from 'connectbase-client'
1032
+
1033
+ // 로그인된 사용자 컨텍스트
1034
+ const cb = new ConnectBase({
1035
+ publicKey: 'cb_pk_...',
1036
+ authToken: appMemberJwt, // AppMember JWT 동봉 시 격리 활성화
1037
+ })
1038
+
1039
+ // 본인 자료만 검색
1040
+ await cb.knowledge.search(kbId, { query: '내 메모' })
1041
+
1042
+ // 본인 자료 + 추가 필터
1043
+ await cb.knowledge.search(kbId, {
1044
+ query: '내 메모',
1045
+ where: { 'metadata.tag': 'work' },
1046
+ })
1047
+ ```
1048
+
1049
+ 자세한 사용법: [docs/knowledge-base/USER_ISOLATION.md](https://github.com/connectbase-world/connectbase/blob/release/docs/knowledge-base/USER_ISOLATION.md)
1050
+
1051
+ ## [3.4.0] - 2026-04-30
1052
+
1053
+ ### Added — `cb.analytics.reset()` 사용자 전환 오염 방지 helper
1054
+
1055
+ 같은 브라우저에서 다른 사용자로 로그인할 때 방문자 (`visitor_uid`) 데이터가 이전
1056
+ 사용자의 식별자에 묶인 채로 남아 데이터가 오염되던 문제를 SDK 측에서 끊어 낼 수
1057
+ 있는 helper 를 추가했습니다.
1058
+
1059
+ - **`cb.analytics.reset()`** — 로그아웃 시 호출. `visitor_uid` 를 새로 발급하고
1060
+ in-flight 큐를 비웁니다.
1061
+ - **자동 reset (1) — `identify(memberId)` 가 다른 멤버 감지** — 직전 세션과 다른
1062
+ `memberId` 가 들어오면 SDK 가 자동으로 `reset()` → identify 순으로 처리.
1063
+ - **자동 reset (2) — `linkMemberSilent` conflict 안전망** — 백엔드가 `409
1064
+ VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 주면 SDK 가 자동으로 reset 후 1회 재시도.
1065
+ 명시적 `reset()` 호출을 누락한 경우의 fallback.
1066
+
1067
+ ### Changed — BatchEvent 멱등성
1068
+
1069
+ - 모든 `BatchEvent` 에 `event_id` (UUID) 자동 부여. 백엔드가 `(visitor_id,
1070
+ event_id)` UNIQUE 인덱스 + `OnConflict DoNothing` 으로 at-least-once 재전송 시
1071
+ 중복 INSERT 차단. SDK 사용자 입장에서 추가 작업 없음.
1072
+
1073
+ ### Fixed — 첫 방문 봇 오판 방지
1074
+
1075
+ - `flushSync` (`navigator.sendBeacon`) 호출에 `user_agent` 를 첨부 — 첫 방문 직후
1076
+ `pagehide` 로 flush 될 때 백엔드가 UA 누락으로 봇으로 분류해 카운트가 누락되던
1077
+ 문제 해결.
1078
+
1079
+ **서버 측 동시 변경:** `LinkMember` idempotent 처리, `WebPageView.event_id` 컬럼 +
1080
+ UNIQUE 인덱스, `link-member` 옵셔널 토큰 검증 (Authorization Bearer 가 있으면
1081
+ AppMember 토큰의 `member_id` 와 body 일치 검증, 없으면 BC 보존 + 경고 로그).
1082
+
1083
+ ## [3.3.1] - 2026-04-30
1084
+
1085
+ ### Fixed — Docs
1086
+
1087
+ 3.3.0 publish 직후 SDK 감사에서 `README.md` 의 `### Game Server` 섹션에 신규 API
1088
+ `cb.game.config` 가 누락된 것을 발견. npm 페이지의 사용자 1차 문서 정합성을 위해
1089
+ patch release 로 보강.
1090
+
1091
+ - README 의 `## API Reference > ### Game Server` 아래에 `#### cb.game.config — Feature
1092
+ Opt-in (v3.1+, SDK 3.3.0+)` 섹션 추가 — get/set/enable/disable 4개 메서드 시그니처,
1093
+ partial PATCH 의미, HTTP 응답 코드 표 (403 / 429 / 402), OPT_IN.md cross-link.
1094
+ - 코드 동작 변화 없음. README/CHANGELOG 만 수정.
1095
+
1096
+ ## [3.3.0] - 2026-04-30
1097
+
1098
+ ### Added — `cb.game.config` (게임 기능 opt-in 토글)
1099
+
1100
+ 게임 서버 v3.1 의 7개 기능 (matchqueue / leaderboard / entity / scripts / voice / replay /
1101
+ spectator) 이 모두 앱 단위 명시적 opt-in 정책으로 전환됨에 따라, 콘솔 / 외부 도구가
1102
+ 토글 상태를 조회/변경할 수 있는 SDK 헬퍼 신규 추가.
1103
+
1104
+ - **`cb.game.config.get(appId?)`** — 현재 7개 토글 상태 + `legacy_defaults` 플래그.
1105
+ row 가 없는 기존 앱은 모든 기능 ON 상태로 응답 (서비스 단절 방지). `legacy_defaults=true`
1106
+ 를 받으면 backfill 권장.
1107
+ - **`cb.game.config.set(appId, patch)`** / **`cb.game.config.set(patch)`** — partial
1108
+ update. 보낸 필드만 갱신, 나머지는 보존. PATCH 직후 game-server 캐시는 NATS
1109
+ publish 로 즉시 무효화 (또는 30s TTL).
1110
+ - **`cb.game.config.enable(appId, feature)`** / **`disable(appId, feature)`** —
1111
+ 단일 토글 편의 wrapper.
1112
+
1113
+ ```ts
1114
+ const cfg = await cb.game.config.get(appId)
1115
+ if (!cfg.matchqueue_enabled) {
1116
+ await cb.game.config.set(appId, { matchqueue_enabled: true, leaderboard_enabled: true })
1117
+ }
1118
+ ```
1119
+
1120
+ 신규 앱은 모든 기능 OFF 가 기본값. 사용 안 하는 앱은 noisy-neighbor / 의도치 않은
1121
+ quota 소비에서 격리됨.
1122
+
1123
+ 자세한 정책: [docs/game-server/OPT_IN.md](https://github.com/connectbase-world/connectbase/blob/release/docs/game-server/OPT_IN.md)
1124
+
1125
+ ### Behavior — 비활성 기능 호출 시 응답 변화
1126
+
1127
+ 본 버전부터, 비활성 feature 의 HTTP 호출은 **HTTP 403 + `{error: "feature_disabled", feature, hint}`**
1128
+ 를 반환. 기존 SDK 호출 코드는 catch 에서 status 403 + error 코드를 분기해 사용자에게
1129
+ "콘솔에서 켜주세요" 메시지를 표시하는 게 권장됩니다.
1130
+
1131
+ ## [3.2.1] - 2026-04-29
1132
+
1133
+ ### Fixed — Docs
1134
+
1135
+ `cb.endpoint.url(label, path)` 의 README/JSDoc 예제와 CHANGELOG `[3.2.0]` 설명을
1136
+ ConnectBase 프록시 인증 모델에 맞춰 정정. 코드 동작 변화 없음 (JSDoc / README /
1137
+ CHANGELOG 만 수정).
1138
+
1139
+ - 3.2.0 의 `<img src={cb.endpoint.url(...)}>` / `new WebSocket(cb.endpoint.url(...))`
1140
+ 예제는 실제로는 401 — `/v1/proxy/:label/*` 가 `X-Public-Key` 헤더를 강제하고
1141
+ 쿼리 파라미터 폴백이 없어, 커스텀 헤더를 못 보내는 브라우저 네이티브 API
1142
+ (`<img>`, native `WebSocket`, `<script src>`, `EventSource`) 는 인증 자체가
1143
+ 불가능. 잘못된 시연 코드를 제거.
1144
+ - 정정된 사용 사례:
1145
+ - **이미지 렌더링**: `cb.endpoint.call(...)` 로 받아
1146
+ `URL.createObjectURL(await res.blob())` 패턴 사용. 영구 URL 이 필요하면
1147
+ `cb.storage.uploadByPath` 로 업로드.
1148
+ - **URL 전달**: Service Worker / 백엔드 워커 / 커스텀 fetch wrapper 처럼 호출자가
1149
+ `X-Public-Key` 헤더를 직접 부착할 수 있는 환경.
1150
+ - **로깅·디버깅**: 라벨 → 최종 URL 매핑 확인.
1151
+ - `EndpointAPI` 클래스 / `cb.endpoint.{call,url,pollUntil}` 동작은 3.2.0 과 동일.
1152
+
1153
+ ## [3.2.0] - 2026-04-29
1154
+
1155
+ ### Added — Endpoint API 헬퍼 (`pollUntil` / `url`)
1156
+
1157
+ ComfyUI × 웹스토리지 같은 e2e 통합 패턴 (작업 제출 → 폴링 → 결과 저장) 을
1158
+ 단일 진입점으로 묶기 위한 헬퍼 2개. `cb.endpoint.call()` 만으로도 가능했지만
1159
+ 사용자가 매번 직접 작성하던 보일러플레이트를 SDK 가 흡수.
1160
+
1161
+ - **`cb.endpoint.pollUntil<T>(label, init, predicate, opts)`** — long-poll 한 줄
1162
+ 처리. ComfyUI `/history/{id}`, A1111 `/sdapi/v1/progress`, 자체 큐 API 처럼
1163
+ "작업 제출 → 폴링" 패턴 전용. `predicate` 가 값을 반환할 때까지 반복 호출,
1164
+ HTTP 5xx/네트워크 오류는 재시도, 4xx 는 즉시 reject, `AbortSignal`/`timeoutMs`
1165
+ 지원. `parse: "json" | "text" | "none"` 으로 본문 파싱 방식 선택.
1166
+ - **`cb.endpoint.url(label, path)`** — 라벨 + path 의 최종 호출 URL
1167
+ (`${baseUrl}/v1/proxy/${label}${path}`) 만 조립해서 반환. URL 을 Service
1168
+ Worker / 백엔드 워커로 넘기거나 로깅·디버깅 용도. ⚠️ `<img src>` / 네이티브
1169
+ `WebSocket` / `<script src>` / `EventSource` 처럼 커스텀 헤더를 못 보내는
1170
+ 브라우저 API 에 직접 넘기면 401 — ConnectBase 프록시는 항상 `X-Public-Key`
1171
+ 헤더를 요구하고 쿼리 파라미터 폴백이 없음. 그 경우엔 `call()` 로 받아
1172
+ `URL.createObjectURL(await res.blob())` 패턴 사용.
1173
+ - 신규 export: `PollUntilOptions` 타입.
1174
+
1175
+ ### Docs
1176
+
1177
+ - `examples/ai-image-generator/` — UMD CDN 한 줄 + 빌드 도구 0 으로 동작하는
1178
+ ComfyUI × 웹스토리지 스타터. SDK 로딩 실패 감지, pre-flight 키 검증, 모든
1179
+ KSampler seed 랜덤화, cache-bust 워크플로우 fetch, AbortController 일괄 취소,
1180
+ step indicator, localStorage 갤러리, ⌘/Ctrl+Enter 단축키 등 11가지
1181
+ 베스트프랙티스 채택.
1182
+ - `docs/integration/comfyui-web-storage.md` — e2e 통합 가이드 + "왜 이 구조가
1183
+ 정답인가" / "안티패턴 7가지".
1184
+
1185
+ ## [3.1.0] - 2026-04-29
1186
+
1187
+ ### Added — Endpoint API (로컬 모델 터널 dumb pipe)
1188
+
1189
+ 사용자 PC GPU 모델 (ComfyUI / A1111 / Hunyuan3D / vLLM / 자체 FastAPI 등) 을
1190
+ `cb_pk_*` 한 키로 호출하는 새 모듈. ConnectBase 는 모델·API·워크플로우를
1191
+ 알지 않고, 라벨 → tunnel 매핑만 들고 페이로드/응답을 그대로 통과시킵니다.
1192
+
1193
+ - **`cb.endpoint.call(label, init)`** — fetch() 시그니처 호환 (path, method,
1194
+ headers, body, signal). URL 은 `${baseUrl}/v1/proxy/${label}${path}` 로
1195
+ 자동 조립, `X-Public-Key` 헤더 자동 주입. SSE / chunked 스트리밍은
1196
+ `res.body.getReader()` 로 그대로 읽기.
1197
+ - 신규 export: `EndpointAPI`, `EndpointCallInit`.
1198
+
1199
+ ### Added — CLI
1200
+
1201
+ - **`connectbase tunnel <port> --label <name>`** — tunnel 발급 후 endpoint
1202
+ binding 을 자동 등록. SDK 사용자가 즉시 `cb.endpoint.call("<label>", { ... })`
1203
+ 로 호출 가능. 인증은 User Secret Key (`cb_sk_*`) — dual-auth 라우트
1204
+ `POST /v1/apps/:appID/endpoints/cli`.
1205
+ - **`--description <text>`** — endpoint binding 의 설명 (`--label` 동반 시만).
1206
+ - 이미 등록된 라벨이면 경고 후 진행 (다른 tunnel_id 로 갱신은 콘솔에서 PATCH).
1207
+
1208
+ ### Fixed
1209
+
1210
+ - JSDoc 안 중첩 블록주석으로 빌드가 깨지던 회귀 수정 (`*/` 가 주석을 조기
1211
+ 종료시키던 케이스).
1212
+
1213
+ ## [3.0.1] - 2026-04-28
1214
+
1215
+ ### Fixed
1216
+
1217
+ - `npx connectbase docs` 가 인증 없이 곧장 문서를 받도록 수정. 백엔드 `/v1/storages/webs/claude-md`
1218
+ 는 public 라우트인데 CLI 가 불필요하게 브라우저 인증 → 앱 선택 → Public Key 발급을 강제하던
1219
+ 흐름을 제거. 캐시된 publicKey 가 있으면 문서에 박아주고, 없으면 백엔드가 placeholder
1220
+ (`YOUR_PUBLIC_KEY_HERE`) 로 대체해 그대로 다운로드.
1221
+
1222
+ ## [3.0.0] - 2026-04-28 — BREAKING
1223
+
1224
+ 게임 서버 mechanism-only 재설계. ConnectBase 가 박아두던 게임 룰 (파티/로비/랭킹/매치메이킹/
1225
+ killcam/highlight) 을 모두 제거하고, primitive (`cb.game.matchqueue`, `cb.game.leaderboard`,
1226
+ `cb.game.scripts`) + 사용자 Lua 로 대체. 1:1 마이그레이션은 [MIGRATION_v3.md](../../../docs/sdk/MIGRATION_v3.md).
1227
+
1228
+ ### Removed (BREAKING)
1229
+
1230
+ - **Lobby**: `listLobbies`, `createLobby`, `getLobby`, `joinLobby`, `leaveLobby`, `toggleReady`,
1231
+ `startGame`, `kickPlayer`, `updateLobby`, `sendLobbyChat`, `invitePlayer`, `acceptInvite`,
1232
+ `declineInvite`, `getPlayerInvites` 메서드 + 관련 타입 (`LobbyInfo`, `CreateLobbyRequest`,
1233
+ `UpdateLobbyRequest`, `LobbyInvite`)
1234
+ - **Party**: `createParty`, `joinParty`, `acceptPartyInvite`, `declinePartyInvite`,
1235
+ `leaveParty`, `kickFromParty`, `inviteToParty`, `getParty`, `getMyParties`, `setReady`,
1236
+ `setPartyMetadata`, `getPartyInvites`, `sendPartyChat` + `PartyInfo`, `PartyInvite`
1237
+ - **Matchmaking**: `joinQueue`, `leaveQueue`, `getMatchStatus` + `JoinQueueRequest`,
1238
+ `MatchmakingTicket`
1239
+ - **Ranking**: `getLeaderboard`, `getPlayerStats`, `getPlayerRank` + `LeaderboardEntry` (구
1240
+ player_id/rating/tier/wins/losses 시그니처. 신규 `LeaderboardScoreEntry` 가 대체).
1241
+ - **Killcam/Highlight**: `cb.game.replay.killcam(...)`, `cb.game.replay.highlights(...)`
1242
+ 관련 endpoint 가 백엔드에서 제거되어 SDK 호출 시 404. 사용자가 `cb.game.replay.download`
1243
+ 로 raw frame 받아 클라/Lua 에서 직접 처리.
1244
+
1245
+ ### Added — v3 primitive
1246
+
1247
+ - **Matchqueue** (`cb.game.matchqueue.*` → `enqueueMatch / listMatchqueue / cancelMatch`):
1248
+ rating/region 등 attributes 는 free-form. 매칭 알고리즘은 사용자 Lua 가 list → notify.
1249
+ - **Leaderboard** (`submitScore / getTopScores / getMemberRank / getRankAround / resetLeaderboard / removeFromLeaderboard`):
1250
+ ELO/티어/시즌 박지 않음. 시즌은 key suffix (`ranks:2026q2`) 로 분리, 점수 공식은 Lua.
1251
+ - **Scripts** (`uploadScript / listScripts / getScript / listScriptVersions /
1252
+ activateScript / rollbackScript / disableScript`): Lua 스크립트 영속 메타 + 버전 + hot reload.
1253
+ - 신규 타입: `MatchqueueTicket`, `MatchqueueListResponse`, `LeaderboardScoreEntry`,
1254
+ `LeaderboardListResponse`, `ScriptMeta`, `ScriptVersion`, `ScriptListResponse`,
1255
+ `ScriptVersionListResponse`, `ScriptDetailResponse`.
1256
+
1257
+ ### Backend 영향
1258
+
1259
+ - game-server v3 부터 `/v1/game/:appID/matchqueue/:key/*`, `/leaderboards/:key/*`,
1260
+ `/scripts/*` 라우트가 유일한 게임 메커니즘 endpoint. lobby/party/ranking/matchmaking
1261
+ 엔드포인트는 모두 404.
1262
+ - 자세한 변경: [`docs/sdk/MIGRATION_v3.md`](../../../docs/sdk/MIGRATION_v3.md),
1263
+ [`docs/game-server/RECIPES.md`](../../../docs/game-server/RECIPES.md).
1264
+
1265
+ ### Migration 요약
1266
+
1267
+ ```ts
1268
+ // Before (v2.x)
1269
+ await cb.game.matchmaking.join({ mode: "ranked", rating: 1500 })
1270
+ await cb.game.ranking.submit({ leaderboard: "elo", score: 2150 })
1271
+
1272
+ // After (v3.0)
1273
+ await cb.game.enqueueMatch(appId, "ranked", userId, { rating: 1500 })
1274
+ await cb.game.submitScore(appId, "elo", userId, 32, "incr")
1275
+ ```
1276
+
1277
+ ## [2.0.0] - 2026-04-27 — BREAKING
1278
+
1279
+ Realtime presence/typing 단일화의 최종 단계. 1.13 deprecation 단계를 건너뛰고 즉시
1280
+ deprecated 코드를 제거. presence/typing 의 단일 SoT 는 `cb.realtime.*`.
1281
+
1282
+ ### Removed (BREAKING)
1283
+
1284
+ - **`cb.database.setPresence(status, device, metadata)`** — 코드 자체 제거. 호출 시 `TypeError`.
1285
+ - **`cb.database.subscribePresence(userIds, callback)`** — 코드 자체 제거.
1286
+ - **`DatabasePresenceState`** type — `PresenceInfo` (from `'./realtime'`) 사용.
1287
+ - 1.13 에서 추가됐던 deprecation `console.warn` 헬퍼도 함께 제거 (필요 없음).
1288
+
1289
+ ### Migration
1290
+
1291
+ ```ts
1292
+ // Before
1293
+ cb.database.setPresence('online', 'web', { nickname: '홍길동' })
1294
+ cb.database.subscribePresence(['user1', 'user2'], (states) => {
1295
+ console.log(states['user1']?.last_seen)
1296
+ })
1297
+
1298
+ // After
1299
+ await cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })
1300
+ const unsub = await cb.realtime.subscribePresence('user1', (info) => {
1301
+ console.log(info.lastSeen)
1302
+ })
1303
+ ```
1304
+
1305
+ 자세한 가이드: [MIGRATION-v2.md](./MIGRATION-v2.md). 필드 차이는 `user_id`→`userId`,
1306
+ `last_seen` (ISO) → `lastSeen` (epoch ms), `'busy'` 상태 추가.
1307
+
1308
+ ### Server-side
1309
+
1310
+ 데이터 서버 측 presence/typing 코드도 함께 제거되었습니다. `presence_set` / `presence_subscribe`
1311
+ / `typing_*` 메시지를 데이터 서버 WebSocket 으로 보내면 `USE_SOCKET_SERVER` 에러가 반환됩니다.
1312
+
1313
+ ## [1.13.0] - 2026-04-27
1314
+
1315
+ Realtime presence/typing 단일화 — `cb.database.setPresence` / `cb.database.subscribePresence`
1316
+ 는 v2.0.0 에서 제거됩니다. 신규 코드는 `cb.realtime.*` 를 사용하세요. 자세한 마이그레이션은
1317
+ [MIGRATION-v2.md](./MIGRATION-v2.md) 참고.
1318
+
1319
+ ### Deprecated
1320
+
1321
+ - **`cb.database.setPresence(status, device, metadata)`** → `cb.realtime.setPresence(status, { device, metadata })`
1322
+ - **`cb.database.subscribePresence(userIds, onPresence)`** → `cb.realtime.subscribePresence(userId, handler)` 또는
1323
+ `cb.realtime.onPresenceChange(handler)` (다중 사용자 일괄 수신).
1324
+ - **`DatabasePresenceState`** type → `PresenceInfo` (from `'./realtime'`).
1325
+ - 필드 네이밍: `user_id` → `userId`, `last_seen` (ISO) → `lastSeen` (epoch ms 숫자).
1326
+ - 상태값: `'busy'` 추가 (PresenceInfo 만 지원).
1327
+
1328
+ 호출 시 개발 환경(`process.env.NODE_ENV !== 'production'`)에서 `console.warn` 이 1회 출력됩니다.
1329
+ 런타임 동작은 1.12.x 와 동일합니다 (이번 버전은 deprecate 만, 코드 제거는 v2.0.0).
1330
+
1331
+ ### Changed
1332
+
1333
+ - **WebSocket URL 기본값** `cb.database.connectRealtime` 의 기본 endpoint 가
1334
+ `/v1/realtime/ws` → `/v1/database/realtime/ws` 로 변경. 기존 경로는 30일간 alias 로
1335
+ 유지되며 응답에 `Sunset` (RFC 8594) + `X-Deprecated-Endpoint` 헤더가 부착됩니다.
1336
+ 명시적으로 `dataServerUrl` 을 지정한 경우 자동 변환은 하지 않습니다 (Hyrum's Law 준수).
1337
+
1338
+ ### Migration
1339
+
1340
+ ```ts
1341
+ // Before (1.12.x)
1342
+ cb.database.setPresence('online', 'web', { nickname: '홍길동' })
1343
+ cb.database.subscribePresence(['user1', 'user2'], (states) => {
1344
+ console.log(states['user1']?.last_seen) // ISO string
1345
+ })
1346
+
1347
+ // After (1.13+, 권장 — v2 호환)
1348
+ cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })
1349
+ const unsubscribe = await cb.realtime.subscribePresence('user1', (info) => {
1350
+ console.log(info.lastSeen) // epoch ms number
1351
+ })
1352
+ ```
1353
+
1354
+ ## [1.12.0] - 2026-04-26
1355
+
1356
+ Analytics 조회 정렬 옵션 정합성 수정 — `VisitorListOptions.sort_by` 의 TypeScript
1357
+ union 을 백엔드가 실제로 인식하는 값으로 정정합니다. 1.10/1.11 의 SDK 타입은
1358
+ `'last_visit' | 'total_visits' | 'total_page_views'` 였으나 백엔드 정렬 분기
1359
+ (`web_visitor_repository.GetStorageVisitors`,
1360
+ `web_visitor_service.GetVisitorGroupsByMember`) 는 `last_visit` 외 값을
1361
+ 인식하지 못해 항상 default(`last_visit`) 분기로 떨어졌습니다 — silent 잘못된 정렬.
1362
+
1363
+ ### Changed — `VisitorListOptions.sort_by` 타입
1364
+
1365
+ - 변경 전 (1.10/1.11): `'last_visit' | 'total_visits' | 'total_page_views'`
1366
+ - 변경 후 (1.12+): `'last_visit' | 'visits' | 'page_views' | 'first_visit'`
1367
+ - 영향 메서드: `cb.analytics.getVisitors()`, `cb.analytics.getVisitorGroups()`.
1368
+ - 런타임은 SDK 가 sort_by 문자열을 그대로 백엔드에 전달하므로, 새 값을 쓰면
1369
+ 비로소 의도한 정렬이 동작합니다. 1.10/1.11 코드에서 `total_visits` /
1370
+ `total_page_views` 를 넘긴 호출은 실제로는 default `last_visit` 정렬 결과를
1371
+ 받고 있었던 점에 주의.
1372
+
1373
+ ### Migration
1374
+
1375
+ ```ts
1376
+ // Before (1.10/1.11): 컴파일은 통과하지만 런타임은 last_visit 정렬
1377
+ cb.analytics.getVisitors('019d8...', { sort_by: 'total_visits' })
1378
+
1379
+ // After (1.12+): 의도한대로 visits 기준 정렬
1380
+ cb.analytics.getVisitors('019d8...', { sort_by: 'visits' })
1381
+ cb.analytics.getVisitors('019d8...', { sort_by: 'page_views' }) // 페이지뷰 누적 기준
1382
+ cb.analytics.getVisitors('019d8...', { sort_by: 'first_visit' }) // 최초 방문일 기준
1383
+ ```
1384
+
1385
+ 기존 코드가 `'last_visit'` 만 사용했다면 변경 불필요 (대부분의 호출).
1386
+ 타입 변경은 breaking 이지만 런타임 호환은 유지됩니다 — 이전 union 의 두 값은
1387
+ 원래 동작하지 않았기 때문.
1388
+
1389
+ ## [1.11.0] - 2026-04-26
1390
+
1391
+ Analytics 사용자 통합 강화 — 멤버별 합산 방문자 조회, 단건 멤버 조회, 즉시 backfill,
1392
+ Visitor merge admin API. sisun 팀의 1.10 이후 후속 피드백이 시작점이며, 어드민에서
1393
+ "회원 단위 합산 방문자" 위젯을 위한 페이지네이션 풀 다운 우회 코드를 SDK 한 줄 호출로
1394
+ 대체할 수 있도록 했습니다. 모든 추가 메서드는 `cb.analytics` 네임스페이스에 들어가며
1395
+ JWT/cb_sk_ dual-auth 로 호출할 수 있습니다.
1396
+
1397
+ ### Added — `cb.analytics.getVisitorGroups(storageWebId?, options?)`
1398
+
1399
+ - 같은 `app_member_id` 의 visitor row 들을 서버 단에서 합산해 단일 row 로 반환.
1400
+ 익명 visitor 는 단일 row 로 그대로 노출되어 페이지네이션 의미가 일관됨.
1401
+ - 응답 필드: `app_member_id`, `visitor_uids[]`, `visitor_count`, `total_visits`,
1402
+ `total_page_views`, `first_visit_at`(MIN), `last_visit_at`(MAX), `country`, `is_bot`.
1403
+ - `visitor_count` 는 **"디바이스 수" 가 아닌 "추적 브라우저 인스턴스 수"** 입니다 (시크릿/일반
1404
+ 모드는 별도 카운트).
1405
+ - 백엔드 `GET /v1/storages/web/:id/visitor-groups` 신설 (dual-auth).
1406
+
1407
+ ### Added — `cb.analytics.getVisitorByMember(storageWebId?, memberId)`
1408
+
1409
+ - 어드민 회원 상세 페이지처럼 한 명만 필요할 때. 페이지네이션 풀 다운 없이 한 번 호출.
1410
+ - 응답은 위 group item 과 동일 형태 (단건).
1411
+ - 백엔드 `GET /v1/storages/web/:id/members/:member_id/visitor` 신설.
1412
+
1413
+ ### Added — `cb.analytics.mergeVisitors(storageWebId?, request)`
1414
+
1415
+ - 두 visitor 가 동일인임을 외부에서 알게 됐을 때 admin 작업으로 통합. source 의 자식
1416
+ 레코드 (page_views, daily, custom_events, experiment_assignments, heatmap_events,
1417
+ session_recordings) 를 target 으로 옮긴 뒤 source 삭제. 단일 트랜잭션, 부분 실패 시
1418
+ 전체 롤백.
1419
+ - 입력: `source_visitor_uid` 필수 + (`target_visitor_uid` 또는 `target_member_id`) 중
1420
+ 하나 필수. `target_member_id` 만 주면 그 멤버의 가장 최근 활동 visitor 를 target 으로 잡음.
1421
+ - 응답: `target_visitor_id`, `moved_records`(이전된 자식 레코드 수).
1422
+ - 제약: `first_visit_at` 은 ent Immutable 필드라 target 값을 바꾸지 않습니다 (daily 가
1423
+ 실제 이력 보존). source/target 은 같은 storage_web 에 속해야 합니다.
1424
+ - 백엔드 `POST /v1/storages/web/:id/visitors/merge` 신설.
1425
+
1426
+ ### Changed — `cb.analytics.identify(memberId)` 동작 보강
1427
+
1428
+ - 1.10 까지: `identify()` 호출 후 다음 batch 가 백엔드에 닿을 때 게스트 visitor 가 회원으로
1429
+ 자동 연결됨 (백엔드 BatchRecordVisit 의 자동 LinkMember 로직).
1430
+ - 1.11+: `identify()` 가 즉시 `POST /visitors/link-member` 를 한 번 호출하여 backfill 을
1431
+ 당겨옵니다. 첫 페이지뷰 전 호출 시 404 발생 가능 — silent fail (다음 batch 가 자가 복구).
1432
+ - `setMemberId(memberId)` 는 즉시 backfill 을 호출하지 않습니다 (이전과 동일 동작).
1433
+
1434
+ ### Migration
1435
+
1436
+ 기존 호출은 그대로 동작합니다. `identify()` 의 즉시 backfill 동작이 추가 HTTP 요청 1번을
1437
+ 유발하므로, 같은 효과를 다음 batch 까지 미루고 싶다면 `setMemberId()` 를 사용하세요.
1438
+
1439
+ ## [1.10.0] - 2026-04-26
1440
+
1441
+ OAuth 콜백 응답 email 노출 + Analytics 조회 메서드 신규 + Functions 자동화용 Push 발송 메서드 신규.
1442
+ sisun 팀(`019d85c7-...`) 의 운영 피드백 3건이 시작점이며, 백엔드 라우트는 콘솔 JWT 와 User Secret Key(`cb_sk_`)
1443
+ 둘 다 받는 dual-auth 로 확장되었습니다. 모든 변경은 추가 성격이며 기존 호출 시그니처는 유지됩니다.
1444
+
1445
+ ### Added — OAuth 콜백 응답에 `email`
1446
+
1447
+ - `OAuthCallbackResponse.email?: string` — `signInWithPopup` / `getCallbackResult` /
1448
+ `exchangeCodeFromCallback` 모두 첫 응답에 이메일을 포함하도록 확장.
1449
+ - 백엔드는 2026-04-19 부터 OAuth 신규 가입/재로그인 시 `AppMember.email` 을 자동 저장해왔으나,
1450
+ 콜백 응답 DTO 에는 노출되지 않아 SDK 사용자가 추가로 `getMember()` 를 호출해야 했음. 이번
1451
+ 릴리즈로 한 번의 로그인 응답만으로 이메일을 확보할 수 있다.
1452
+ - Apple private relay / 이메일 권한 미동의 시 `email` 은 비어 있을 수 있다 (선택 필드).
1453
+
1454
+ ### Added — Analytics 조회 메서드 (Functions / cb_sk_ 전용)
1455
+
1456
+ - `cb.analytics.getPopularPages(storageWebId?, options?)` — 인기 페이지.
1457
+ - `cb.analytics.getNavigationFlow(storageWebId?, options?)` — 페이지 전환 플로우 (Sankey).
1458
+ - `cb.analytics.getVisitors(storageWebId?, options?)` — 방문자 목록.
1459
+ - 백엔드 라우트 `/v1/storages/web/:id/{popular-pages,navigation/flow,visitors}` 가 dual-auth 로 변경되어
1460
+ 콘솔 JWT 외에 User Secret Key(`cb_sk_`) 호출도 허용. 어드민 앱이 Function 에서 호출해 자체
1461
+ 대시보드를 구성할 수 있다.
1462
+ - 브라우저 SDK (Public Key `cb_pk_`) 인스턴스에서는 호출 시 명확한 에러를 던져 권한 누설을 차단.
1463
+
1464
+ ### Added — Push 발송 메서드 `cb.push.sendToMembers`
1465
+
1466
+ - `cb.push.sendToMembers(appId, memberIds, payload)` — Functions / 서버 자동화 환경에서
1467
+ 회원 ID 목록으로 푸시 발송. payload 는 title/body/imageUrl/data/platforms/ttlSeconds/priority/
1468
+ clickAction/scheduledAt 지원.
1469
+ - 백엔드 `/v1/apps/:appID/push/send` 가 cb_sk_ 인증을 받도록 dual-auth 로 변경. 1.9 까지 안내되던
1470
+ `CONSOLE_ACCESS_TOKEN` (만료 있는 콘솔 JWT) 우회 패턴이 더 이상 필요 없음.
1471
+ - Public Key 인스턴스 호출 시 명확한 에러로 차단.
1472
+
1473
+ ### Changed — 문서
1474
+
1475
+ - `embedded/sections/15-sdk-push.md` — Functions 발송 예제를 cb_sk_ 권장 패턴으로 갱신, legacy
1476
+ `CONSOLE_ACCESS_TOKEN` 마이그레이션 안내 추가.
1477
+ - `embedded/sections/33-sdk-analytics.md`, `38-sdk-analytics-advanced.md` — REST 표에
1478
+ visitors/popular-pages/navigation/flow 라우트의 dual-auth 를 명시.
1479
+
1480
+ ### Migration
1481
+
1482
+ 기존 호출은 그대로 동작합니다.
1483
+
1484
+ - OAuth: `signInWithPopup()` 결과의 `email` 이 신규 추가 — 사용 안 해도 무방.
1485
+ - Analytics 조회: 1.9 까지 SDK 에 조회 메서드가 없었으므로 신규 도입.
1486
+ - Push 발송: Functions 코드의 `fetch + Authorization: Bearer ${CONSOLE_ACCESS_TOKEN}` 패턴은
1487
+ 유지 가능하지만, secret 을 `cb_sk_` 로 교체 후 SDK 메서드로 옮기는 것을 권장 (만료 없음).
1488
+
1489
+ ## [1.9.1] - 2026-04-25
1490
+
1491
+ 문서 정합성 패치. 런타임 동작은 1.9.0 과 동일합니다.
1492
+
1493
+ ### Fixed — `ConnectBaseConfig.persistence` jsdoc 모순
1494
+
1495
+ - `src/index.ts` 의 `persistence?: TokenPersistence` jsdoc 이 `@default 'localStorage'` 로 표기되어 있었지만, 실제 `HttpClient` 동작은 `?? 'none'` (메모리 저장) 으로 fallback (`src/core/http.ts:104-107`). 같은 패키지 내 [src/core/http.ts:21-29](src/core/http.ts) 의 doc-comment 는 `'none' (권장·기본값)` 으로 올바르게 표기되어 있었음.
1496
+ - IDE/타입 hint 가 잘못된 정보를 안내해 사용자가 새로고침 시 로그아웃되는 동작을 "버그" 로 오인할 수 있었음.
1497
+ - 두 doc-comment 를 일치시켜 `@default 'none'` 으로 정정. 보안·XSS 메모도 함께 명시.
1498
+ - **Behavior change 없음** — 기본 동작은 1.6.0 이후 줄곧 `'none'` 이었습니다.
1499
+
1500
+ ## [1.9.0] - 2026-04-24
1501
+
1502
+ 런타임 내구성·관측성 강화 릴리스. 공개 API 시그니처는 유지되며 모든 변경은 추가·하드닝 성격(비파괴).
1503
+
1504
+ ### Added — 요청 타임아웃 & AbortSignal
1505
+
1506
+ - `ConnectBaseConfig.requestTimeoutMs` — 모든 HTTP 호출의 기본 타임아웃(ms). 기본 30000ms, 0/음수 시 비활성.
1507
+ - 개별 호출에서 `AbortOptions` (`signal`, `timeout`) 지원 — Storage presigned 업로드 등 장시간 호출을 외부에서 취소 가능.
1508
+ - `core/abort.ts`: `createTimeoutController`, `DEFAULT_REQUEST_TIMEOUT_MS` 유틸.
1509
+
1510
+ ### Added — 전역 에러 관찰 훅
1511
+
1512
+ - `ConnectBaseConfig.onError` — 모든 `ApiError` / `AuthError` 발생 시 호출되는 옵저버. Sentry/Datadog 등 관측성 파이프라인 연결용.
1513
+ - 429 응답 개선: `Retry-After` 헤더(초 또는 HTTP-date)를 파싱해 `ApiError.details.retry_after_seconds` 로 전달.
1514
+
1515
+ ### Added — 응답 shape 검증 (fail-fast)
1516
+
1517
+ - `core/validate.ts`: `assertShape` 도입. 서버가 필수 필드를 누락한 응답을 반환해도 즉시 throw 하여 이후 로직이 `undefined` 로 조용히 실패하지 않게 방어.
1518
+ - 적용 지점: `auth.signInMember`, `auth.getMe`, `payment.createCheckoutSession`, `push.registerDevice`, `queue.consume`, `subscription.create`.
1519
+
1520
+ ### Added — Presigned URL 스킴 검증 (SSRF 하드닝)
1521
+
1522
+ - `core/url-validation.ts`: `validateExternalUrl`, `isLocalhostOrigin`.
1523
+ - `StorageAPI` presigned PUT 호출 전에 URL 스킴(https)을 검증해 서버 응답을 맹신하는 경로를 차단. localhost 허용 여부는 런타임 오리진 기반으로 자동 결정.
1524
+
1525
+ ### Changed — 에러 타입 통일
1526
+
1527
+ - `FunctionsAPI.invokeAndWait`, `GameAPI.listRooms` / `getRoom` — `throw new Error(...)` → `throw new ApiError(status, message, code)`. `ApiError` 는 `Error` 를 상속하므로 `instanceof Error` / `.message` 로 잡던 기존 코드는 그대로 동작하며, 추가로 `error.code` / `error.statusCode` 로 분기 가능.
1528
+ - `VideoAPI` 내부 `request<T>()` — 서버의 구조화 응답 `{ error: { code, message, details } }` 을 완전히 언랩해 `ApiError(status, message, code, details)` 로 변환. timeout 도 함께 적용.
1529
+
1530
+ ### Changed — Refresh 토큰 회복성
1531
+
1532
+ - `HttpClient` 에 지수 백오프 도입 — 연속 refresh 실패 시 500ms × 2^n (최대 30s) 동안 재요청을 차단해 서버에 실패 요청이 쏟아지는 회귀를 막는다. 성공 시 카운터 리셋.
1533
+ - Refresh 요청 자체도 `requestTimeoutMs` 로 제한.
1534
+
1535
+ ### Changed — 로깅
1536
+
1537
+ - `RealtimeAPI`: 내부 `console.error` 를 `private logError()` 로 통일. `options.debug` opt-in 일 때만 출력되어 프로덕션 devtools 에 SDK 로그 노출이 0.
1538
+
1539
+ ### Docs
1540
+
1541
+ - `AnalyticsAPI.flush` / `getSession`, `VideoAPI.list` / `getStreamUrl` 에 상세 JSDoc + `@example` 추가.
1542
+
1543
+ ### Compatibility
1544
+
1545
+ - **Non-breaking**: 모든 공개 메서드 시그니처 동일. `Error` → `ApiError` 전환은 상속 관계상 기존 `catch (e: Error)` 경로에 영향 없음.
1546
+ - **Runtime behavior change**: 서버가 필수 응답 필드를 누락하는 경우, 이전 버전은 `undefined` 로 조용히 진행했지만 1.9.0 은 즉시 throw 한다. 서버 응답이 정상이라면 차이 없음.
1547
+
1548
+ ## [1.8.1] - 2026-04-23
1549
+
1550
+ 문서 정합성 패치. SDK 런타임 동작은 1.8.0 과 동일합니다.
1551
+
1552
+ ### Changed — README
1553
+
1554
+ - `createData` 예제에 `DataItem` 반환 (id/created_at/updated_at 즉시 사용 가능) 설명 추가.
1555
+ - `createMany` 예제 신설 — 반환 shape `{ created: DataItem[], total, success }` 명시.
1556
+ - `updateData` 예제를 반환값을 받아 사용하는 형태로 정비.
1557
+
1558
+ ## [1.8.0] - 2026-04-19
1559
+
1560
+ 웹 스토리지 CLI 배포(`connectbase deploy ./dist`) 를 manifest 기반 **증분 업로드** 로 재구성. 동일 dist 재배포는 거의 즉시 완료되고, 일부만 바뀐 경우 변경분만 전송한다. 서버 DB 쓰기도 단일 트랜잭션 + bulk insert 로 묶어 전체 배포 시간이 짧아짐.
1561
+
1562
+ ### Added — 증분 배포
1563
+
1564
+ - **`GET /v1/public/storages/webs/:storageID/deploy/manifest`** — 현재 배포된 파일의 `path/size/hash(sha256)` 와 결정적 `revision` 반환.
1565
+ - **`POST /v1/public/storages/webs/:storageID/deploy/incremental`** — `{ upsert, delete, base_revision? }` 변경분만 적용. 서버가 hash 를 재검증하고 단일 트랜잭션으로 upsert/delete 처리 후 기존 배포 파이프라인으로 Object Storage 에 전체 업로드.
1566
+ - CLI 는 prod 배포 시 자동으로 manifest → diff → incremental 순서를 사용한다. 변경 없음이면 `✓ 변경사항 없음` 출력 후 종료.
1567
+ - `base_revision` 은 옵션. 동시 배포 경합에서 `409 Conflict` 면 manifest 재조회 후 1회 자동 재시도.
1568
+
1569
+ ### Changed — 서버 배포 속도 개선 (기존 `/deploy` 포함)
1570
+
1571
+ - `CLIDeploy` 가 파일/폴더별 개별 트랜잭션을 단일 트랜잭션 + depth-batch `CreateBulk` 로 교체. 수십~수백 개의 `BEGIN/COMMIT` 왕복이 1회로 축소.
1572
+ - 실패 시 기존 파일도 함께 롤백되어 **실패한 배포가 스토리지를 비우는 회귀가 사라짐** (과거엔 DeleteAll 이 선행되고 이후 SaveFile 이 실패하면 스토리지가 빈 상태로 남았음).
1573
+
1574
+ ### Compatibility
1575
+
1576
+ - **구버전 서버**: manifest 엔드포인트 404 → 기존 `/deploy` 전량 업로드로 자동 폴백.
1577
+ - **구버전 SDK** (`1.7.x` 이하): 기존 `/deploy` 는 그대로 동작 (내부 로직만 최적화됨).
1578
+ - **Dev 배포** (`connectbase deploy --dev`) 는 Object Storage 에 직접 업로드하는 구조상 증분 적용 불가 — 항상 전량 업로드 유지.
1579
+
1580
+ ### Internal — DB 스키마
1581
+
1582
+ - `storage_web_file.content_hash` 컬럼 추가 (Optional, Default `""`). 기존 row 는 manifest 최초 조회 시 lazy backfill.
1583
+
1584
+ ## [1.7.0] - 2026-04-19
1585
+
1586
+ 콘솔(JWT) DB 관리 API 경로 정정 — 기존 메서드들이 실제로 존재하지 않는 라우트(`/v1/apps/:appID/tables/:tableID/...`, `/v1/apps/:appID/triggers`, `/v1/apps/:appID/security/rules`, `/v1/apps/:appID/tables/:tableID/relations`) 를 호출해 서버가 항상 404 를 반환하던 문제를 수정.
1587
+
1588
+ ### Fixed — 404 를 반환하던 메서드들이 이제 정상 동작
1589
+
1590
+ 실제 백엔드 라우트는 모두 `/v1/apps/:appID/databases/...` prefix 아래에 있습니다 ([route/api/v1.go:617-679](../../../backend/cmd/core-server/app/route/api/v1.go#L617)). SDK 가 이 prefix 를 빠뜨리고 있었습니다.
1591
+
1592
+ - **`database.listIndexes` / `createIndex` / `deleteIndex` / `analyzeIndexes`** — 경로 `/v1/apps/:appID/tables/:tableID/indexes*` → `/v1/apps/:appID/databases/tables/:tableID/indexes*`
1593
+ - **`database.listSearchIndexes` / `createSearchIndex` / `deleteSearchIndex`** — 동일 패턴 수정
1594
+ - **`database.listGeoIndexes` / `createGeoIndex` / `deleteGeoIndex`** — 동일 패턴 수정
1595
+ - **`database.listTriggers` / `createTrigger` / `updateTrigger` / `deleteTrigger`** — 경로 `/v1/apps/:appID/triggers*` → `/v1/apps/:appID/databases/triggers*`
1596
+ - **`database.listSecurityRules` / `createSecurityRule` / `updateSecurityRule` / `deleteSecurityRule`** — 경로 `/v1/apps/:appID/security/rules*` → `/v1/apps/:appID/databases/security/rules*`
1597
+
1598
+ ### Breaking — relations API 시그니처 변경
1599
+
1600
+ 실제 백엔드 `relations` 라우트는 app 전역(`/v1/apps/:appID/databases/relations`) 에 있고 관계는 UUID(`relation_id`) 로 식별됩니다. 기존 SDK 시그니처는 존재하지 않는 table-scoped 경로 + 관계 이름으로 삭제를 시도해 호출 자체가 불가능한 상태였습니다.
1601
+
1602
+ - **`database.listRelations(appId, tableId)` → `database.listRelations(appId, sourceTable?)`** — 두 번째 인자는 필터용(선택), 생략 시 앱 전체 관계 반환. `sourceTable` 은 query string `?source_table=` 으로 전달됩니다.
1603
+ - **`database.createRelation(appId, tableId, data)` → `database.createRelation(appId, data)`** — `tableId` 인자 제거. 테이블 정보는 body 의 `source_table`/`target_table` 로 전달.
1604
+ - **`database.deleteRelation(appId, tableId, relationName)` → `database.deleteRelation(appId, relationId)`** — `relationId` 는 `listRelations` 가 반환하는 UUID (`id` 필드). 기존 `(tableName, alias)` 조합으로 지정할 수 없습니다.
1605
+
1606
+ ### Migration
1607
+
1608
+ 기존 호출이 실제로 성공하는 경로가 없었으므로(항상 404) 깨지는 호출자는 없어야 합니다. relations 를 쓰던 소비자는 위 새 시그니처로 교체하고, triggers/indexes/security-rules 는 코드 변경 없이 자동으로 동작하게 됩니다.
1609
+
1610
+ ## [1.6.0] - 2026-04-19
1611
+
1612
+ 웹 방문 추적에 로그인 회원을 자동 연결 — 콘솔의 **앱 멤버 > 활동기록 > 웹방문기록** 탭이 이제 실제로 채워진다. 이전까지는 SDK 가 배치 이벤트를 `visitor_uid` 만 실어 보내 `web_visitors.app_member_id` 가 전부 NULL 로 남아 있었다.
1613
+
1614
+ ### Added
1615
+
1616
+ - **`analytics.setMemberId(id | null)`**: 로그인/로그아웃 시점에 방문자 트래커에 회원 ID 를 전달한다. 이후 모든 페이지뷰/이벤트 배치 (`/v1/public/storages/web/{id}/visitors/batch`) 요청에 `app_member_id` 필드가 포함되어 서버가 게스트 방문자를 회원과 자동 연결한다.
1617
+ - **`analytics.getMemberId()`**: 현재 트래커에 설정된 회원 ID 조회. 로그인 상태 확인용.
1618
+
1619
+ ### Changed
1620
+
1621
+ - **`auth.signUpMember/signInMember/signInAsGuestMember` 가 내부적으로 `analytics.setMemberId()` 호출**: 기존 `window.__cbSetMember` 전역 콜백만 호출하던 경로가 실제로 배치 큐에 회원 ID 를 적용하도록 연결됨. `auth.signOut()` 시에는 `setMemberId(null)` 로 익명 상태 복귀. `ConnectBase` 생성자에서 `auth._attachAnalytics(analytics)` 로 자동 연결.
1622
+ - **`analytics.identify(memberId)` 가 `setMemberId` 로 단순화**: 동작 동일하나 내부 구현이 새 경로로 통합. 기존 호출부는 그대로 동작.
1623
+
1624
+ ### Why
1625
+
1626
+ 기존 구현은 `auth.ts` 가 `window.__cbSetMember(memberId)` 를 호출하면 "끝" 이었는데, `AnalyticsAPI` 에는 이 전역 함수를 받을 경로도 없고 로그인 후 쌓이는 배치 이벤트에도 회원 ID 가 첨부되지 않았다. 그 결과 `web_visitors.app_member_id` 가 항상 NULL 이라 앱 멤버 페이지의 방문기록 탭이 텅 비어 있었다. 이제 로그인이 일어나면 그 시점부터의 모든 이벤트 배치에 `app_member_id` 가 자동으로 따라간다.
1627
+
1628
+ ## [1.5.0] - 2026-04-19
1629
+
1630
+ 터널의 proxy_token UX 정리 — 콘솔 AI Config 경로(95%+ 사용자)는 토큰을 건드리지 않고, curl/웹훅처럼 외부에서 터널 URL 을 직접 호출하는 소수 케이스에만 명시적 플래그로 opt-in 하도록 분리.
1631
+
1632
+ ### Added
1633
+
1634
+ - **`--public` 플래그**: proxy_token 검증을 비활성화한 채 터널을 연다. Stripe/GitHub 등 커스텀 헤더를 못 붙이는 웹훅 수신용. tunnel-server 가 `?public=1` 쿼리를 받아 `Tunnel.Public=true` 로 등록하고 proxy_handler 가 토큰 검증을 skip. 활성화 시 CLI 는 눈에 띄는 노란 경고를 출력 (`cli.ts` handleMessage, `backend/cmd/tunnel-server/app/handler/proxy_handler.go`).
1635
+ - **`--show-token` 플래그**: proxy_token 값과 `curl -H "X-Proxy-Token: ..."` / `?proxy_token=` 예시를 CLI 에 출력. curl 로 터널을 직접 때려보고 싶을 때만 사용.
1636
+ - **감사 로그·메트릭**: 공개 모드 터널 세션 생성 시 `tunnel_public_opened_total{app_id}` 카운터 증가 + 매 요청마다 remote IP/method/path 가 `Public tunnel access` 로그로 남음.
1637
+
1638
+ ### Changed
1639
+
1640
+ - **터널 기동 시 proxy_token 기본 숨김**: 1.4.1 에서 추가했던 "토큰 + curl 예시 자동 출력"을 제거. 기본 터널 사용자는 콘솔 AI Config 가 서버에서 자동으로 토큰을 resolve 하므로 CLI 에 노출할 필요가 없다. 필요하면 `--show-token` 으로 명시적으로 꺼냄.
1641
+ - **`tunnel_ready` 프로토콜 메시지에 `public` 필드 추가**: 서버가 CLI 에 현재 세션의 공개 여부를 내려주어 CLI 가 올바른 안내를 출력하도록 함 (`protocol/message.go`).
1642
+
1643
+ ### Why
1644
+
1645
+ (1) 1.4.1 의 토큰 자동 출력은 기본 플로우(콘솔 AI Config)에서는 "내가 뭘 해야 하나?" 혼란을 유발했다. 대부분 사용자는 토큰 존재조차 모르는 게 맞다. (2) Stripe/GitHub 처럼 커스텀 헤더를 못 실어 보내는 웹훅 수신처는 `?proxy_token=` 쿼리 번거로움 + 세션 재시작 시 재등록 문제가 있었다. `--public` 으로 opt-in 할 수 있게 해 이 케이스를 깔끔히 분리.
1646
+
1647
+ ## [1.4.2] - 2026-04-18
1648
+
1649
+ `npx connectbase docs` 명령이 init/deploy/tunnel 과 달리 brower auth 흐름을 거치지 않아, Public Key 가 없는 사용자가 키 발급 경로를 모른 채 prompt 만 보던 UX 문제 해결.
1650
+
1651
+ ### Fixed
1652
+
1653
+ - **`docs` 가 키 없을 때 자동 발급 흐름을 트리거**: `.connectbaserc.publicKey` → `secretKey` 가 있으면 앱 선택만, 없으면 `browserAuthFlow()` → 앱 선택/생성 → Public Key 신규 발급 → `.connectbaserc` 저장까지 자동 수행. 한 번 실행하면 다음부터는 추가 입력 없이 통과 (`cli.ts` `ensureDocsPublicKey`).
1654
+ - **`tunnelAppId` 캐시 공유**: docs 와 tunnel 이 동일한 `.connectbaserc.tunnelAppId` 를 사용하므로 한쪽에서 앱을 선택하면 다른 쪽에서도 재사용된다.
1655
+ - **죽은 fallback 정리**: `config.publicKey ?? config.publicKey` (자기 자신 fallback), `!config.publicKey && !config.publicKey` (동일 변수 두 번 검사) 제거.
1656
+
1657
+ ### Internal
1658
+
1659
+ - `resolveAppForTunnel` → `resolveApp` 으로 일반화. 새 앱 생성 시 백엔드(`POST /v1/public/cli/apps`) 응답에 포함된 `public_key` 를 같이 반환하도록 시그니처 확장. tunnel 호출부는 동작 동일.
1660
+
1661
+ ## [1.4.1] - 2026-04-18
1662
+
1663
+ `connectbase tunnel` CLI 가 공개 URL 호출에 필요한 **proxy token** 을 표시하지 않아, 사용자가 토큰을 알 방법이 없어 모든 요청이 `401 invalid or missing proxy token` 으로 막히던 문제 해결.
1664
+
1665
+ ### Fixed
1666
+
1667
+ - **`tunnel_ready` 출력에 proxy token 노출**: 터널 활성화 시 서버가 내려보낸 세션 단위 토큰을 stdout 에 표시합니다 (`cli.ts` `handleMessage`). 이전에는 `tunnel_handler.go:300-308` 가 `proxy_token` 을 보내주지만 CLI 가 무시해서 사용자가 토큰을 확인할 경로가 사실상 없었습니다.
1668
+ - **사용 예시 안내**: 공개 URL 호출 시 사용해야 하는 헤더(`X-Proxy-Token`)와 쿼리(`?proxy_token=`) 형식을 `curl` 예시로 함께 출력합니다. 백엔드 `proxy_handler.go:231-239` 가 검증하는 입력은 이 두 가지뿐이며 `Authorization`, `Cookie`, Basic Auth 는 받지 않습니다.
1669
+
1670
+ ### Notes
1671
+
1672
+ - 토큰은 터널 세션 단위로 서버가 새로 생성합니다. 세션 종료(WebSocket disconnect) 시 즉시 무효화되므로 stdout 노출에 따른 추가 위험은 거의 없습니다.
1673
+ - 백엔드 / 콘솔 / 문서 측 변경은 없습니다 — 클라이언트 표시만 보강.
1674
+
1675
+ ## [1.4.0] - 2026-04-18
1676
+
1677
+ `FetchDataResponse` 타입을 실제 서버 wire 포맷에 맞추는 타입 정정.
1678
+
1679
+ ### Fixed
1680
+
1681
+ - **`FetchDataResponse` 필드명을 서버 응답에 맞춤**: `datas` → `data`, `total_size` → `total_count`.
1682
+ - data-server 는 `/v1/public/tables/:tableID/data` (GET) 및 `/v1/public/tables/:tableID/data/query` (POST) 에서 실제로 `{ data: [...], total_count: N }` 를 반환합니다 (`internal_data_controller.go:598-604`, `845-848`). 따라서 이전 타입 정의(`datas` / `total_size`)를 기대한 구조분해는 런타임에 `undefined` 를 받고 있었습니다.
1683
+ - `0.16.0` 에서 한 번 바로잡았다가 `0.16.1` 에서 "서버 wire 가 여전히 `datas`" 라는 잘못된 관찰로 롤백됐던 이슈를 다시 수정합니다. 서버 핸들러 (`gin.H{"data": ..., "total_count": ...}`) 를 직접 확인해 타입을 확정했습니다.
1684
+
1685
+ ### Breaking (types only, runtime unchanged)
1686
+
1687
+ - `FetchDataResponse` 의 필드명이 TypeScript 레벨에서 변경됩니다. 런타임 동작(실제 네트워크 응답)은 동일합니다. 기존에 타입 경고를 무시하고 `result.data` / `result.total_count` 로 접근하던 코드는 **수정 없이 바로 동작**합니다. 반대로 타입 정의를 믿고 `result.datas` / `result.total_size` 로 접근하던 코드는 원래부터 런타임에 `undefined` 를 받고 있었으므로, 이번 기회에 `result.data` / `result.total_count` 로 고쳐주세요.
1688
+
1689
+ ### Migration
1690
+
1691
+ ```ts
1692
+ // Before (타입은 OK 였지만 런타임 undefined)
1693
+ const { datas, total_size } = await cb.database.getData(tableId)
1694
+
1695
+ // After (v1.4.0+)
1696
+ const { data, total_count } = await cb.database.getData(tableId)
1697
+ ```
1698
+
1699
+ ## [1.3.0] - 2026-04-18
1700
+
1701
+ 파티 초대 수락/거절 SDK 메서드 추가. 기존 `acceptInvite` 는 로비 전용(`/lobbies/invites/...`) 이라 파티 초대에는 사용할 수 없었는데, 1.2.0 까지는 사용자가 직접 `fetch()` 로 백엔드의 `/v1/game/:appID/invites/:inviteID/accept` 를 호출해야 했음. 1.3.0 부터 SDK 에 전용 메서드 제공.
1702
+
1703
+ ### Added
1704
+
1705
+ - **Party**: `cb.game.acceptPartyInvite(inviteId, playerId, displayName?)` — `inviteToParty` 로 생성된 초대를 수락하여 파티 합류. 백엔드 엔드포인트 `POST /v1/game/:appID/invites/:inviteID/accept` (query string 으로 `player_id`, `display_name` 전달).
1706
+ - **Party**: `cb.game.declinePartyInvite(inviteId, playerId)` — 파티 초대 거절.
1707
+
1708
+ ### Changed
1709
+
1710
+ - `cb.game.joinParty` throw 메시지를 `acceptPartyInvite` 플로우 안내로 갱신 (기존 `acceptInvite` 권고는 잘못된 안내였음 — 해당 메서드는 로비 전용).
1711
+
1712
+ ## [1.2.0] - 2026-04-18
1713
+
1714
+ Replay SDK 4개 메서드 활성화. 백엔드가 `REPLAY_STORAGE_PATH` 환경변수로 replay 저장소를 구성한 경우에만 동작하며, 미설정 시 SDK 가 명시적 에러를 throw (404 분기).
1715
+
1716
+ ### Added
1717
+
1718
+ - **Replay**: `cb.game.listReplays/getReplay/downloadReplay/getReplayHighlights` 활성화 (backend `/v1/game/:appID/replays/*`). 서버의 `REPLAY_STORAGE_PATH` 설정 여부에 따라 파일 스토리지 기반으로 동작. 서버 미구성 시 `cb.game.listReplays: replay storage is not configured on this server (REPLAY_STORAGE_PATH unset).` 형태의 명확한 에러 발생.
1719
+
1720
+ ## [1.1.0] - 2026-04-18
1721
+
1722
+ game-server 의 party/spectator/ranking/voice public 라우트 오픈에 맞춰 SDK 의 throw 해제. 백엔드 미제공 기능은 여전히 throw 로 남김.
1723
+
1724
+ ### Added
1725
+
1726
+ - **Party**: `cb.game.createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화 (backend `/v1/game/:appID/parties/...`). `joinParty` 는 백엔드에 직접 join 엔드포인트가 없어서 `inviteToParty` → `acceptInvite` 플로우로 대체 — `joinParty` 호출 시 해당 안내 Error throw.
1727
+ - **Spectator**: `cb.game.joinSpectator/leaveSpectator/getSpectators` 활성화 (backend `/v1/game/:appID/rooms/:roomID/spectators`).
1728
+ - **Ranking**: `cb.game.getLeaderboard/getPlayerStats/getPlayerRank` 활성화. **시그니처 변경** — 백엔드가 `game_type` 쿼리를 필수로 요구하므로 SDK 에서도 첫 인자로 `gameType` 을 받음. `getLeaderboard(gameType, top?, season?)`, `getPlayerStats(playerId, gameType, season?)`, `getPlayerRank(playerId, gameType, season?)`.
1729
+ - **Voice**: `cb.game.joinVoiceChannel` 활성화 (backend `/v1/game/:appID/voice/rooms/:roomID/join`).
1730
+
1731
+ ### Still disabled (throws Error — 백엔드 public 경로 미제공)
1732
+
1733
+ - `cb.game.createRoom`, `cb.game.deleteRoom` — 콘솔 전용
1734
+ - `cb.game.joinParty` — 초대 수락 플로우 사용
1735
+ - `cb.game.listReplays`, `getReplay`, `downloadReplay`, `getReplayHighlights` — replay 스토리지 아직 미연결
1736
+
1737
+ ## [1.0.0] - 2026-04-18
1738
+
1739
+ 문서-코드 정합성 9라운드 감사 후속. 런타임 동작이 실제 백엔드와 일치하지 않던 메서드들을 정정. `GoogleConnectionStatus` shape 변경은 breaking change 이므로 major bump.
1740
+
1741
+ ### Breaking
1742
+
1743
+ - **`ads.getConnectionStatus()` 반환 타입 중첩 구조로 변경**: 백엔드 실제 응답(`{adsense, admob}`) 과 불일치하여 `.admob_account_id` 접근 시 `undefined` 였음. 이제 `status.adsense.is_connected`, `status.adsense.account_id`, `status.admob.is_connected`, `status.admob.account_id`, `status.admob.publisher_id` 로 접근. `AdsenseConnectionInfo`, `AdmobConnectionInfo` 타입 추가 export.
1744
+ - **game.ts 다수 메서드에 public 경로 미오픈 Error throw 적용** (호출 시 즉시 Error): `createRoom`, `deleteRoom`, `createParty`/`joinParty`/`leaveParty`/`kickFromParty`/`inviteToParty`/`sendPartyChat`, `joinSpectator`/`leaveSpectator`/`getSpectators`, `getLeaderboard`/`getPlayerStats`/`getPlayerRank`, `joinVoiceChannel`, `listReplays`/`getReplay`/`downloadReplay`/`getReplayHighlights`. 기존에도 백엔드 경로가 admin 전용이라 404 였으나, 침묵 실패 대신 명시적 에러로 전환. 해당 기능은 콘솔에서 진행하거나 백엔드 public 경로 오픈 요청 필요.
1745
+
1746
+ ### Fixed
1747
+
1748
+ - **`webrtc.getICEServers()` / `getStats()` / `getRooms()` 경로 정정**: 각각 `/v1/ice-servers`, `/v1/apps/:appID/webrtc/stats`, `/v1/apps/:appID/webrtc/rooms` 로 호출하여 404. 실제 webrtc-server 는 `/v1/apps/:appID/ice-servers`, `/stats`, `/rooms` 로 노출됨. `getICEServers` 는 이제 `this.appId` 를 사용하며 없으면 Error.
1749
+ - **`storage.moveFile()` / `renameFile()` Public Key 404 명시적 에러**: 해당 라우트는 `/v1/public/...` 에 노출되어 있지 않음. Public Key 만 있고 JWT 없을 때 호출하면 이제 즉시 명시적 Error. JWT 인증 시 `/v1/storages/...` 로 정상 호출.
1750
+
1751
+ ### Changed
1752
+
1753
+ - `HttpClient.hasJWT()` 메서드 추가 — Access Token(JWT) 존재 여부 확인용.
1754
+ - `payment.prepare()` JSDoc 예제를 Toss Payments V2 시그니처(`payment.requestPayment({ method, amount: { currency, value }, ... })`) 로 갱신.
1755
+
1756
+ ### Documentation
1757
+
1758
+ - `KnowledgeSearchRequest`, `ImportDataRequest` 타입에 JSDoc `@example` 추가 — IDE 자동완성 툴팁 보강.
1759
+
1760
+ ## [0.16.1] - 2026-04-17
1761
+
1762
+ MCP/SDK 문서-코드 정합성 검증에서 발견된 불일치 소규모 정정. 런타임 동작 변경은 없으며, 이전 릴리스에서 도입된 타입 회귀를 롤백.
1763
+
1764
+ ### Fixed
1765
+
1766
+ - **`FetchDataResponse` 필드명 회귀 롤백**: `0.16.0` 에서 `datas→data`, `total_size→total_count` 로 바꿨으나 실제 서버 wire 는 여전히 `datas` / `total_size` 였음. 타입 정의를 서버 응답 그대로 `{ datas, total_size }` 로 되돌려 런타임 미스매치 제거.
1767
+ - `cb.push.subscribeTopic()` JSDoc 예제가 `device_token` 인자 누락된 구 시그니처로 표기되던 문제 정정 (실 시그니처는 `subscribeTopic(deviceToken, topic)`).
1768
+ - CLI 안내 문구·`setupMonorepoRoot` 로 추가되는 `cb:update`/`cb:docs`/`cb:mcp` 스크립트가 `connectbase-client` 를 가리키던 것을 정규 bin 이름인 `connectbase` 로 통일. 두 bin 은 모두 동작하지만 문서와 자동 생성 스크립트는 단일 이름 기준으로 일관화.
1769
+
1770
+ ### Documentation
1771
+
1772
+ - README `### Push Notifications` 섹션의 예제를 실제 공개 API 와 일치하도록 수정: `push.register()` / `push.subscribeToTopic()` / `push.unsubscribeFromTopic()` (존재하지 않는 메서드) → `push.registerDevice()` / `push.subscribeTopic(deviceToken, topic)` / `push.unsubscribeTopic(deviceToken, topic)` / Web Push 등록 예제 추가.
1773
+
1774
+ ### Changed
1775
+
1776
+ - `package.json` 메타데이터 보강: `author`, `homepage`, `bugs`, `sideEffects: false` 필드 추가, `files` 에 `LICENSE`/`CHANGELOG.md`/`README.md` 명시적 포함.
1777
+ - 저장소 루트에 `LICENSE` (MIT) 파일 신설 (이전에는 `license: "MIT"` 선언만 존재).
1778
+
1779
+ ## [0.16.0] - 2026-04-17
1780
+
1781
+ CLI `update` 커맨드와 신규 저장소 API 옵션, 토큰 영속성 옵션 추가. 일부 타입 정정.
1782
+
1783
+ ### Added
1784
+
1785
+ - **CLI `update` 커맨드**: 현재 설치된 `connectbase-client` 버전을 npm 과 비교하고 `docs`·`mcp` 산출물을 일괄 최신화.
1786
+ - **모노레포 지원**: `connectbase init --setup-root` 로 모노레포 루트 `package.json` 에 `cb:update`/`cb:docs`/`cb:mcp` 편의 스크립트 설치.
1787
+ - **Storage `getFiles()` `parentId` 옵션**: 특정 폴더 하위 파일 목록 조회 지원.
1788
+ - **토큰 영속성(persistence) 옵션**: `new ConnectBase({ persistence: 'localStorage' | 'sessionStorage' | 'none' })`.
1789
+ - `localStorage` (기본값): 브라우저 종료 후에도 토큰 유지.
1790
+ - `sessionStorage`: 탭 종료 시 삭제.
1791
+ - `none`: 메모리에만 저장.
1792
+ - 생성자에서 저장된 토큰 자동 복원 (명시적 토큰 전달 시 스킵). SSR 환경(`window` undefined) 안전 처리.
1793
+ - **OAuthProvider 확장**: `kakao`, `apple` 프로바이더 추가 (서버 enum 과 동기화).
1794
+
1795
+ ### Changed
1796
+
1797
+ - `VERSION` 상수가 `package.json` 에서 동적으로 로드되도록 변경 (이전: `0.10.6` 하드코딩).
1798
+ - `FetchDataResponse` 필드명 변경: `datas` → `data`, `total_size` → `total_count` (서버 응답과 일치를 의도함).
1799
+ - **⚠️ 본 변경은 실제 서버 wire 와 맞지 않는 회귀였으며 `0.16.1` 에서 롤백됩니다.** `0.16.0` 사용자는 `0.16.1` 이상으로 즉시 업그레이드 권장.
1800
+
1801
+ ### Documentation
1802
+
1803
+ - SDK 문서 전체 감사 및 누락 섹션 보강.
1804
+
1805
+ ## [0.15.0] - 2026-04-16
1806
+
1807
+ Web Analytics SDK 모듈 신설 및 터널 유틸 리팩터.
1808
+
1809
+ ### Added
1810
+
1811
+ - **AnalyticsAPI** 모듈 (`cb.analytics`): `init` / `trackEvent` / `trackPageView` / `identify` / `setConsent` / `enableHeatmap` / `enableHeartbeat` / `destroy`.
1812
+ - **SessionManager**: 30분 타임아웃, `visitor_uid` localStorage 영속화.
1813
+ - 백엔드 측 12개 서비스·9개 Ent 스키마(이벤트/퍼널/세션/코호트/어트리뷰션/세그먼트/A-B 테스트/히트맵/녹화/프라이버시) 와 매칭.
1814
+
1815
+ ### Changed
1816
+
1817
+ - CLI 내부에서 tunnel 관련 로직을 `src/tunnel-utils.ts` 로 추출 (테스트 용이성 확보).
1818
+ - `vitest` 도입 및 tunnel/analytics 단위 테스트 23건 추가.
1819
+
1820
+ ## [0.14.0] - 2026-04-15
1821
+
1822
+ Tunnel 안정성 개선.
1823
+
1824
+ ### Added
1825
+
1826
+ - **Tunnel lockfile**: 앱+포트 기반 lockfile 로 동일 터널 중복 실행을 차단. stale lockfile 자동 감지(PID 생존 확인).
1827
+ - `--force` 플래그: lockfile 무시 옵션.
1828
+
1829
+ ### Changed
1830
+
1831
+ - 서버의 `tunnel_error code=replaced` 수신 시 재연결을 중단하고 안내 메시지 출력 (이전에는 끊임없이 재연결 시도).
1832
+ - `TunnelMessage` 타입에 `code` / `error` / `message` 필드 추가.
1833
+
1834
+ ## [0.13.0] - 2026-04-14
1835
+
1836
+ Database API 정합성 및 완성도 대규모 개선.
1837
+
1838
+ ### Breaking Changes
1839
+
1840
+ - **`TableSchema` 응답 구조 전면 재정의**: 서버 ent 모델 (`backend/cmd/data-server/ent/schema/table.go`) 과 1:1 일치하도록 변경.
1841
+ - `name` → `title`
1842
+ - `columns: ColumnSchema[]` → `schema: TableSchemaDefinition` (평면 또는 중첩 맵)
1843
+ - `created_at` → `create_time` (※ G2 작업에서 다시 `created_at` 로 표준화 예정)
1844
+ - `updated_at` → `update_time`
1845
+ - 신규 필드: `app_id`, `access_level`, `is_active`, `validation_schema?`
1846
+ - **`CreateTableRequest` 형태 변경**: 서버 DTO 와 일치.
1847
+ - `schema?: TableSchemaDefinition` 신규 필드. 평면 맵 (`{email: 'string'}`) 또는 중첩 객체 (`{email: {type: 'string', required: true}}`) 모두 지원. `$required` 키로 필수 컬럼 지정.
1848
+ - `accessLevel?: 'Creator' | 'Public' | 'AppMember'` (기본 `'Creator'`)
1849
+ - `description` 은 `@deprecated` — 서버에 저장되지 않음
1850
+ - **`createTable()` 반환 타입**: `Promise<TableSchema>` → `Promise<void>`. 서버는 `{message}` 만 반환하므로 거짓 타입 제거.
1851
+ - **`createColumn()` / `updateColumn()` 반환 타입**: 동일한 이유로 `Promise<void>` 로 변경.
1852
+ - **`updateTable()` 시맨틱 정정**: PATCH 부분 업데이트가 정상 동작. 이전 버전에서는 모든 필드가 required 라 부분 업데이트 시 400 오류가 발생했음.
1853
+ - **`DataType` 정렬**: 서버 `ValidSchemaTypes` 와 일치하도록 변경.
1854
+ - 제거: `'boolean'`
1855
+ - 추가: `'int'`, `'bool'`, `'uuid'`
1856
+ - 최종 union: `'string' | 'int' | 'number' | 'bool' | 'uuid' | 'date' | 'object' | 'array'`
1857
+ - **`CreateColumnRequest`**:
1858
+ - `is_required` 가 optional 로 변경 (기본 false)
1859
+ - `default_value` 타입 `string` → `unknown` (서버는 임의 타입 허용)
1860
+ - `order`, `validation_rule` 은 `@deprecated` (서버 미지원)
1861
+ - **`UpdateColumnRequest`**:
1862
+ - `default_value` 타입 `string` → `unknown`
1863
+ - `name` `@deprecated` (서버는 컬럼 rename 미지원)
1864
+ - `order`, `validation_rule` `@deprecated`
1865
+ - **`ColumnSchema`**:
1866
+ - SDK 가 합성하는 객체임을 명시
1867
+ - `default_value` 타입 `string` → `unknown`
1868
+ - `created_at` 은 테이블의 `create_time` 으로 대체 (컬럼 개별 타임스탬프 없음)
1869
+ - `order` 는 SDK 가 부여한 인덱스
1870
+ - `updated_at`, `validation_rule` `@deprecated`
1871
+
1872
+ ### Added
1873
+
1874
+ - `TableSchemaDefinition` interface — 컬럼 정의 맵 타입. 플랫 / 중첩 / `$required` 모두 지원.
1875
+ - `TableColumnDef` union type — 단일 컬럼 정의 (플랫 string 또는 중첩 객체).
1876
+ - `TableAccessLevel` type — `'Creator' | 'Public' | 'AppMember'`.
1877
+ - `database.createTable()` 가 초기 schema 와 access level 을 한 번에 받아 1-step 생성 지원.
1878
+ - `database.getColumns()` 가 플랫 / 중첩 schema 모두 정확히 파싱.
1879
+ - `database` 모듈의 `getPublicPrefix()` 가 항상 `/v1/public` 반환 (이전: 인증 종류에 따라 `/v1/public` 또는 `/v1` — 후자는 dead path).
1880
+ - `database.getValidationSchema()`, `setValidationSchema()`, `deleteValidationSchema()` — 테이블 검증 스키마 CRUD.
1881
+ - `MemberInfoResponse` 에 `email?`, `role?` 필드 추가 — RLS `auth.email`/`auth.role` 과 일치.
1882
+ - MCP `update_column` 툴 신설 (description, encrypted, data_type, is_required, default_value 변경).
1883
+ - MCP `get_validation_schema`, `set_validation_schema`, `delete_validation_schema` 툴 신설.
1884
+
1885
+ ### Fixed
1886
+
1887
+ - **🔴 `database.createTable()` 가 어떤 키로도 동작하지 않던 broken 상태 해결**.
1888
+ - Public Key 경로: 요청 body 가 서버 DTO 와 불일치 (필드명 `name`/`title`, 누락 `access_level`, 빈 `schema`)
1889
+ - Secret Key 경로: 존재하지 않는 `/v1/tables` 경로 호출 → 404
1890
+ - 두 경로 모두 본 릴리스에서 정상 동작.
1891
+ - **MCP `create_table` 툴이 빈 schema 거부로 항상 실패** 하던 문제 해결 (서버 hook 이 explicit 빈 맵을 거부했음). 서버 DTO/repository/MCP tool 세 곳을 동시에 수정.
1892
+ - 서버 측 `CreateTableRequest.Schema` 가 `binding:"required"` 였으나 schema 생략 가능하도록 변경.
1893
+ - 서버 측 `EditTableInternal` (PATCH) 가 full-replace 시맨틱이라 부분 업데이트 불가능했던 문제 해결. `PatchTableRequest` DTO 와 `PatchTable` repository 메서드 신설.
1894
+ - MCP `create_column`, `update_column` 툴의 `data_type` enum 이 `'boolean'` 을 포함했으나 서버는 `'bool'` 만 허용 → 정정.
1895
+ - `getColumns()` 가 플랫 schema (`"email": "string"`) 를 모두 `'string'` 으로 잘못 반환하던 문제 해결.
1896
+
1897
+ ### Documentation
1898
+
1899
+ - `secretKey` 의 JSDoc 명확화: 앱 DB API 에는 사용 불가, CLI / tunnel 전용임을 명시.
1900
+ - embedded SDK 문서 (04-sdk-database.md, 11-mcp-tools.md, 25-app-packaging.md) 의 `boolean` → `bool`, `auth.email`/`auth.role` 정합성 정리, `createTable` 예제 갱신.
1901
+ - `TableSchemaDefinition` 의 `$required` 키 사용법, `TableColumnDef` 평면/중첩 형태 모두 JSDoc 예제 포함.
1902
+
1903
+ ### Notes
1904
+
1905
+ 본 변경의 대부분은 backwards-incompat 이지만, 이전 버전 (`0.12.x`) 의 `createTable` 자체가 broken 상태였으므로 해당 메서드를 사용하던 코드는 어차피 동작하지 않았습니다. read 메서드 (`getTables`, `getTable`, `getColumns`) 는 사용자 코드가 escape-hatch 캐스팅 (`as any`) 으로 우회하던 경우만 영향받습니다.
1906
+
1907
+ ---
1908
+
1909
+ ## [0.12.2] - 2026-04-?? (이전)
1910
+
1911
+ 이전 릴리스 — 본 CHANGELOG 도입 이전. git history 참조.