# Changelog

본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.

## [4.4.1] - 2026-07-26

### Fixed — 로그인·가입 응답의 refresh 쿠키가 저장되지 않아 세션 복구가 항상 실패하던 문제

`persistence: 'none'`(권장·기본값) + `autoRestoreSession: true` 조합에서 **새로고침하면 항상
로그아웃**되던 버그를 고쳤다 (platform-issue 019f9e23).

`fetchCredentialsForPath` 가 `/v1/public/` prefix 전체를 `credentials: 'omit'` 으로 보내고 있었는데,
로그인·가입 엔드포인트(`/v1/public/app-members/signin` · `signup`)가 바로 그 아래였다. `omit` 으로
보낸 요청은 브라우저가 응답의 `Set-Cookie` 를 **폐기**하므로 멤버 refresh 쿠키가 아예 저장되지
않았고, 이후 `/v1/auth/re-issue` 는 쿠키가 없어 401 이 됐다. 즉 "안전한 기본값" 과 "동작하는
세션" 중 하나를 포기해야 하는 상태였다.

- 쿠키를 주고받는 공개 인증 경로(`signin` / `signup` / `signout`)만 화이트리스트로 `'include'`.
- 그 밖의 `/v1/public/*` 는 그대로 `'omit'` — `*.web.connectbase.world` 배포 시 부모 도메인
  콘솔 쿠키가 딸려오는 회귀(019ea60d)는 계속 차단된다.

> 서버 측에도 짝이 되는 수정이 함께 배포됐다. `/v1/auth/re-issue` 의 인증 미들웨어가 앱별
> 쿠키 이름(`cb_member_refresh_token_<appID>`) 대신 legacy 단일 이름만 찾고 있어서, 쿠키가
> 저장된 뒤에도 401 이 났다. SDK 만 올리면 동작하지 않으므로 두 수정이 모두 필요하다.

## [4.4.0] - 2026-07-25

### Changed — `cb.publicKey.*` 를 service_role 함수에서 호출 가능 (`publickey:read` / `publickey:manage`)

퍼블릭 키 단위 `payment_mode`(4.3.0)가 생긴 뒤에도 **키를 만드는 경로가 콘솔뿐**이라, "QA 빌드에는
test 결제 키" 를 배포 파이프라인이 자동화할 수 없었다(platform-issue 019f9961). 키 관리 REST 를
service_role 함수(`ctx.cbAdmin`)에 열고, 최소권한 스코프를 추가했다.

- 함수 `management_scopes` 에 `publickey:read`(목록/모드 조회) / `publickey:manage`(생성·수정·삭제)
  를 opt-in 하면 `ctx.cbAdmin.publicKey.*` 가 동작한다. 스코프가 없으면 403.
- `cb.publicKey.*` 에 위임 인증 가드 추가 — Public Key(cb_pk_) 단독 인스턴스(브라우저)에서
  호출하면 즉시 에러를 던진다(키가 자기 자신을 발급/수정하는 경로 차단). 콘솔 JWT 와
  service_role 경로는 그대로 동작한다.
- `cb_sk_`(시크릿 키) 단독 호출은 여전히 401 — 시크릿을 자동화/AI 컨텍스트로 흘리지 않는 정책 유지.

AI 도구를 쓰는 경우 MCP `create_public_key`(`payment_mode` 파라미터 추가) /
`list_public_keys`(신규) / `update_public_key`(신규) 로 같은 작업을 할 수 있고, 생성 응답과 목록에
`payment_mode` 가 포함돼 "QA 키가 정말 test 인지" 되읽어 검증할 수 있다.

## [4.3.0] - 2026-07-25

### Added — 퍼블릭 키 단위 결제 모드 (`payment_mode`)

키마다 결제 자격증명 모드를 고정할 수 있다: `inherit`(기본, 앱 설정을 따름) | `test` | `live`.
같은 앱에서 QA 빌드는 테스트 결제, 프로덕션 빌드는 라이브 결제로 **동시에** 운영할 수 있다 —
지금까지는 모드가 앱×프로바이더 단위 단일 값이라, 라이브 전환 이후 결제 회귀 테스트를 하려면
콘솔에서 모드를 되돌려야 했고 그동안 실고객 결제가 테스트로 처리됐다.

모드를 정하는 주체가 서버에 저장된 키 설정이라 클라이언트는 고를 수 없다 — 브라우저가 test 를
골라 무료로 "결제 성공"을 만드는 우회는 여전히 불가능하다.

- `CreatePublicKeyRequest.payment_mode`, `UpdatePublicKeyRequest.payment_mode`
- `PublicKeyItem.payment_mode`, `CreatePublicKeyResponse.payment_mode`,
  `UpdatePublicKeyResponse.payment_mode`
- `PublicKeyPaymentMode` 타입 export

### Added — 구독의 결제 모드 (단건결제와 대칭)

구독에는 모드 필드가 아예 없어 **어떤 경로로도 테스트 구독을 만들 수 없었다** — 유일한 스위치가
콘솔의 프로바이더 모드였고, 그것을 되돌린 사이 실고객 결제가 테스트로 처리됐다.

- `CreateSubscriptionRequest.payment_mode` — 단건결제 `prepare()` 의 `payment_mode` 와 적용
  조건이 완전히 동일하다(서버 시크릿 키 호출에서만 적용, 브라우저 공개 키 호출에서는 무시).
- `SubscriptionResponse.payment_mode` — 이 구독이 고정한 모드. 프로바이더 중립 필드라 목록에서
  테스트 구독과 실구독을 가를 수 있다(`environment` 는 Paddle 전용이라 쓸 수 없었다).
- 키 단위 결제 모드는 결제뿐 아니라 **구독 생성·빌링키 발급에도 동일하게 적용**된다.

### Added — 결제 금액의 통화·정산·국가 정보

MoR(Paddle/Dodo)은 고객에게 **표시통화**로 청구하고 머천트에게는 환산된 **정산통화**로 지급한다.
지금까지는 정산통화 금액이 표시통화 라벨을 달고 나가 앱이 거짓 금액을 표시할 수 있었다
(예: `$10.10` 이 `₩1,010` 으로).

- `PaymentDetail.settlement_amount` / `.settlement_currency` / `.country`
- `PaymentListItem.currency` (기존에는 금액만 있고 통화가 없었다)
- `SubscriptionResponse.country` / `.mode`
- `SubscriptionPaymentResponse.provider` / `.settlement_amount` / `.settlement_currency` / `.country`
- `ChangePlanPreview.recurring_currency` / `.settlement_amount` / `.settlement_currency`

### Added — Dodo 요금제 변경 미리보기

`cb.subscription.previewChangePlan()` 이 Dodo 구독에서도 동작한다(기존에는 Paddle 전용).

- `ProrationMode` 에 `difference_immediately` 추가 (Dodo 전용 — 차액만 즉시 청구)

### Changed

- `ChangePlanPreview.currency` 는 **즉시청구액의 통화**로 명확해졌다. 새 정기 단가의 통화는
  `recurring_currency` 를 쓴다(비어 있으면 `currency` 와 동일).
- 프로바이더가 지원하지 않는 `proration_mode` 는 근사 대체 없이 400 `invalid_proration_mode` 다.
  이전에는 Dodo 에서 `prorated_next_billing_period`("다음 청구일에 부과")가
  `difference_immediately`("지금 차액 청구")로 조용히 바뀌어, 요청하지 않은 즉시 출금이 발생했다.
- 미리보기 미지원 프로바이더의 에러 코드가 `plan_change_unsupported` → `plan_change_preview_unsupported`
  로 분리됐다. 전자는 "요금제 변경 자체가 불가", 후자는 "미리보기만 없고 `changePlan` 은 동작"이다.

## [4.2.0] - 2026-07-25

### Added — 패키징 앱 네이티브 API 보강 (`cb.native.*`)

앱 패키징(React Native/Electron) 셸이 제공하는 브릿지를 SDK 에서 그대로 쓸 수 있도록 빠져 있던
네임스페이스를 채웠다. 전부 additive — 기존 코드는 변경 없이 동작한다.

- `cb.native.location.watchPosition(onUpdate, options?)` / 반환된 핸들의 `clear()` — 위치 추적.
  모바일 패키징 앱에서는 네이티브가 `nativeLocationUpdate` 이벤트로 갱신을 스트리밍하고, 웹/데스크톱은
  Geolocation `watchPosition` 으로 폴백한다. (기존에는 SDK 에 watch 래퍼가 아예 없었고, 브릿지는
  위치 갱신을 요청-응답으로 보내 첫 좌표 이후가 전부 유실됐다.)
- `cb.native.push.*` — `getToken()`(raw APNS/FCM 토큰, `cb.push.registerDevice` 에 그대로 사용) /
  `requestPermission()` / `scheduleLocal()` / `setBadgeCount()`.
- `cb.native.getAppInfo()` — 패키징 앱의 플랫폼·OS·앱 버전·웹 URL (웹에서는 `null`).
- `cb.native.isNativeApp()` — 패키징 앱 안에서 실행 중인지.
- `NativeAppInfo`, `LocationWatch` 타입 export.

### Changed

- `cb.native.notification.show()` — 모바일 패키징 앱에서 WebView 가 지원하지 않는 Web Notification
  API 대신 네이티브 로컬 알림으로 전달한다(푸시 기능이 켜진 앱). 기존에는 모바일에서 조용히 실패했다.
- `cb.native.shell.openExternal()` — 모바일 패키징 앱에서 `window.open` 대신 시스템 브라우저로 연다.
- `cb.native.filesystem.saveFile()` — 모바일에서 파일명만 넘기면 앱 문서 디렉터리에 저장된다
  (네이티브 셸이 상대 경로를 해석). 기존에는 절대 URI 가 아니라 저장이 실패했다.
- `NativeBridgeInterface` 타입에 `os` / `isNative` / `call` / `app.getInfo` / `app.reload` /
  `browser.openExternal` / `filesystem.getDirectories` / `filesystem.getAppDir` /
  `push.setBadgeCount` 추가 — 패키징 셸의 실제 브릿지 표면과 일치시켰다.

## [4.1.0] - 2026-07-25

### Added — 결제 자격증명 test/live 모드 (`payment_mode`)

앱이 프로바이더마다 **테스트 키와 라이브 키를 따로 등록**해두고 모드만 전환하는 모델을 지원한다.
사용할 모드는 콘솔에서 프로바이더별로 정하며, 결제 응답에 어느 모드로 처리됐는지 함께 내려온다.

**추가:**
- `PaymentMode` 타입 (`"test" | "live"`).
- `PreparePaymentRequest.payment_mode` — 사용할 모드 오버라이드(선택). **서버 시크릿 키(`cb_sk_*`)
  호출에서만 적용**되고, 브라우저 공개 키(`cb_pk_*`) 호출에서는 무시된다(클라이언트가 test 를 골라
  실제 돈 없이 "결제 성공"을 만드는 엔타이틀먼트 우회 방지).
- `PreparePaymentResponse.payment_mode` — 이 결제가 사용한 모드.
- `PaymentDetail.payment_mode` — 결제 조회 응답에 모드 포함.
- `ListPaymentsOptions.mode` — 결제 목록을 모드로 필터(테스트 결제를 걸러 실매출만 보기).

기존 코드는 변경 없이 동작한다 — 필드를 지정하지 않으면 콘솔에 설정된 프로바이더 모드를 따른다.

## [4.0.0] - 2026-07-23

### Changed — ⚠️ BREAKING: MoR 결제 필드 프로바이더 중립화 + Dodo Payments 지원

Paddle 전용이던 결제/구독 필드를 프로바이더 중립(MoR = Merchant of Record) 이름으로 정규화하고,
`dodo`(Dodo Payments)를 새 MoR 프로바이더로 추가했다. 결제 엔진을 Paddle↔Dodo(향후 Polar/Lemon
Squeezy) 간에 코드 변경 없이 바꿀 수 있다.

**필드 리네임 (subscription / payment 응답·요청):**

| 이전 (`paddle_*`) | 이후 (중립) |
|---|---|
| `paddle_price_id` | `provider_price_ref` |
| `paddle_transaction_id` | `provider_ref` |
| `paddle_client_token` | `client_token` |
| `paddle_environment` | `environment` |
| `paddle_checkout_url` | `checkout_url` |
| `new_paddle_price_id` | `new_provider_price_ref` |

**추가:**
- checkout 응답에 `launch_mode`(`"overlay"` | `"redirect"`) — 프론트가 결제창 여는 방식을 결정
  (Paddle=오버레이 client_token, Dodo=리다이렉트 checkout_url).
- provider 유니온에 `"dodo"`.

**마이그레이션:** `cb.subscription.create/changePlan/previewChangePlan`, `cb.payment.prepare/confirm`
사용부에서 `paddle_price_id`→`provider_price_ref`, `paddle_transaction_id`→`provider_ref`,
`paddle_client_token`→`client_token`, `paddle_environment`→`environment`,
`paddle_checkout_url`→`checkout_url` 로 교체. `proration_mode`/`on_payment_failure` 값 어휘는 그대로
(프로바이더 중립 어휘로 문서화).

## [3.54.0] - 2026-07-23

### Added — 구독 요금제 스왑 + Paddle proration (`cb.subscription.changePlan` / `previewChangePlan`)

기존 구독을 **하나로 유지한 채(id 동일) 다른 요금제로 갈아타고 남은 금액을 즉시 일할 정산(proration)**
하는 메서드를 추가했다. Paddle(MoR) 구독 전용. 예) 혼자→가족 in-place 업그레이드 — 이미 낸 혼자
미사용분을 Paddle 이 자동 크레딧해 첫 가족 청구에서 차감한다. 기존 `create`/`update`/`cancel` 등에는
영향 없는 **순수 additive** 변경.

- `cb.subscription.changePlan(subscriptionId, { paddle_price_id, plan_name?, amount?, proration_mode?, on_payment_failure?, metadata? })`
  — 요금제 스왑 + 정산 적용. 내부적으로 Paddle `PATCH /subscriptions/{id}` 의 items 전체 교체 +
  `proration_billing_mode` 로 처리하고, `subscription.updated`/`transaction.completed` 웹훅으로 재조정한다.
- `cb.subscription.previewChangePlan(subscriptionId, data)` — 실제 적용 없이 정산 결과
  (지금 청구/크레딧 순액 · 새 정기 청구액 · 다음 청구일)를 계산. 세금·기존 크레딧까지 반영된 Paddle
  계산값이라 확정 화면에 그대로 노출하면 된다.
- 신규 타입: `ChangePlanRequest`, `ChangePlanPreview`, `ProrationMode`(`prorated_immediately`(기본) |
  `prorated_next_billing_period` | `full_immediately` | `full_next_billing_period` | `do_not_bill`),
  `OnPaymentFailure`(`prevent_change`(기본) | `apply_change`).
- `SubscriptionResponse` — 현재 물린 recurring price 를 나타내는 `paddle_price_id` 필드 추가.

> 참고: toss/stripe/payapp/paypal 구독은 PG 네이티브 proration 이 없어 미지원이다(`plan_change_unsupported`).
> Paddle 구독이 최초 결제(checkout) 전이면 아직 Paddle 측 구독이 없어 `subscription_not_activated` 를 반환한다.

## [3.53.0] - 2026-07-23

### Changed — 웹 스토리지 배포: `--dev` → QA 채널 별칭(deprecated)

웹 스토리지 배포를 **QA → promote 불변배포 단일 파이프라인**으로 수렴했다. 막다른 길이던
레거시 dev 배포(별도 `.dev.web.connectbase.world` 호스트 + `{id}/dev` 저장경로)는 서버에서
제거됐고, `--dev` 플래그는 남아 있으나 이제 **QA 채널 배포의 별칭**으로 동작한다.

- `deploy --dev` 는 `promote_mode` 와 무관하게 이번 릴리스를 **QA 포인터에만** 반영한다
  (production 미반영). 실행 시 deprecation 안내를 출력하며, 검증 후 `promote` 로 production
  에 반영한다. **deprecated** — 정식 흐름 `deploy`(신규 스토리지 기본 `manual` → QA)
  → `releases` → `promote` 사용을 권장한다.
- `deploy` / `promote` / `releases` 명령의 인터페이스와 프로그래매틱 API 는 변경 없음.

> 참고(서버 측): 신규 웹 스토리지는 기본 `promote_mode=manual` 로 생성된다 — `deploy` 는
> QA 에만 반영되고 production 은 명시적 `promote` 로만 바뀐다. 기존 스토리지는 영향 없다.

## [3.52.0] - 2026-07-22

### Added — Paddle(MoR) 임베디드 결제 타입

앱 임베디드 결제·구독 응답에 Paddle(Merchant of Record) 프로바이더 타입을 추가했다. 서버가
Paddle 트랜잭션을 만들어 반환하면, 앱에서 Paddle.js 오버레이 checkout 을 띄워 결제받을 수 있다
(결제 확정의 원천은 서버 webhook). 기존 toss/stripe/payapp/paypal 사용에는 영향 없는 **순수 additive** 변경.

- `PaymentProvider` 유니온에 `"paddle"` 추가.
- `PreparePaymentResponse` — `paddle_transaction_id`, `paddle_client_token`, `paddle_environment`(sandbox|production), `paddle_checkout_url` 필드 추가.
- `ConfirmPaymentRequest` — `paddle_transaction_id` 필드 추가(트랜잭션 상태 확인용).
- `SubscriptionResponse` — `provider` 에 `paddle` 포함, `paddle_transaction_id`/`paddle_client_token`/`paddle_environment`/`paddle_checkout_url` 추가. (부수적으로 누락됐던 `paypal` provider 값도 정합.)
- `CreateSubscriptionRequest` — `paddle_price_id`(Paddle 카탈로그 recurring price ID) 필드 추가.

## [3.50.0] - 2026-07-18

### Added — 파일 업로드 진행률 콜백 (`cb.storage.uploadFile` onProgress)

영상 업로드(`cb.video.upload`)만 제공하던 진행률 콜백을 파일 스토리지 업로드에도 추가했다. 파일 업로드 UI에서 실시간 퍼센트(%) 진행 바를 그릴 수 있다. (platform-issue 019f70e3)

- `cb.storage.uploadFile(storageId, file, options)` — 3번째 인자가 부모 폴더 ID(문자열) 또는 옵션 객체를 받는다. **기존 `uploadFile(id, file, 'folder-id')` 시그니처와 하위 호환**.
- 옵션: `onProgress?: (p: { loaded, total, percentage }) => void`, `parentId?`, `signal?`(AbortSignal, 취소), `timeout?`(PUT 타임아웃 ms).
- `onProgress` 는 브라우저(XMLHttpRequest) 환경에서 presigned PUT 의 `upload.onprogress` 로 바이트 단위 진행률을 실시간 보고한다. Node/Edge 등 XHR 이 없는 환경에서는 시작 0% / 완료 100% 만 통지(계약 유지).
- `cb.storage.uploadByPath(storageId, path, file, options)` 및 `cb.storage.uploadFiles()` 도 동일한 `onProgress`/`signal`/`timeout` 을 지원한다(uploadFiles 는 파일별 0→100%).
- `percentage` 필드명은 `cb.video.upload` 의 `UploadProgress` 와 통일. 신규 타입 export: `StorageUploadProgress`, `UploadFileOptions`. `UploadByPathOptions` 에 `onProgress`/`signal`/`timeout` 추가.

## [3.47.0] - 2026-07-16

### Added — ctx.cbAdmin 관리 REST (service_role + management_scopes)

service_role 함수가 시크릿 키 없이 일부 관리 REST 를 호출할 수 있도록 `ctx.cbAdmin` 에 관리 메서드를 추가했다. 함수 생성 시 `management_scopes` 로 필요한 스코프만 opt-in 하면 되고(최소권한), 시크릿 키는 대화/로그/코드 어디에도 등장하지 않는다. (platform-issue 019f6a82)

- `cb.push.getStats(appId)` — 푸시 통계(도달/오픈율/클릭율, 디바이스/메시지/토픽 수). 스코프 `push:read`. 신규 타입 `PushStatsResult`.
- `cb.roles` (신규 모듈) — 앱 역할(RBAC) 관리. 조회 `list`/`get`(스코프 `role:read`), 변경 `create`/`update`/`assign`/`delete`(스코프 `role:manage`). `assign` 은 기존 제목/설명/권한을 보존한 채 사용자만 교체한다. 신규 타입 `RoleList`/`RoleListItem`/`RoleDetail`/`RolePermissionItem`/`RoleUserItem`/`CreateRolePayload`/`CreateRoleResult`/`UpdateRolePayload`.
- `cb.payment.list(appId, options?)` — 결제 내역 목록(필터/페이지네이션). 스코프 `payment:read`. 신규 타입 `PaymentListItem`/`PaymentListResult`/`ListPaymentsOptions`.
- 세 메서드 모두 서버사이드 전용 — Public Key(cb_pk_) 단독 브라우저 인스턴스는 클라이언트 단에서 차단하고, 위임 Bearer(ctx.cbAdmin / 콘솔 JWT) 인스턴스만 허용한다(sendToMembers 와 동일 가드).

## [3.46.0] - 2026-07-11

### Added — 크로스 플랫폼 음성 인식(STT) API

패키징된 WebView 앱(특히 iOS WKWebView)에서 `webkitSpeechRecognition` 이 없어 음성 입력이 불가능하던 문제를 해결하기 위해 `cb.native.speech` 를 추가했다.

- `cb.native.speech.isAvailable()` — 음성 인식 지원 여부
- `cb.native.speech.recognize({ lang, interim, continuous, onPartial })` — 음성 인식 시작, 최종 transcript 반환. `onPartial` 로 중간 결과 실시간 수신.
- `cb.native.speech.stop()` — 진행 중 인식 중지
- 플랫폼 자동 분기: 웹/데스크톱=Web Speech API, 패키징 모바일 앱=네이티브 브릿지(iOS `SFSpeechRecognizer` / Android `SpeechRecognizer`). 패키징 시 `native_features.speech` 필요.
- 신규 타입 export: `SpeechRecognizeOptions`, `SpeechResult`.

### Changed

- `NativeBridge.push.getToken()` 반환 타입에 `platform`('ios' | 'android') 추가. 패키징 셸이 이제 raw APNS/FCM 디바이스 토큰을 제공하므로 `cb.push.registerDevice({ device_token, platform })` 에 그대로 전달 가능.

## [3.44.0] - 2026-07-03

### Added — CLI 비대화형 init + SDK 문서 수명주기 정비

AI 에이전트(Claude Code 등)가 `init` 을 직접 실행할 수 있도록 비대화형 모드를 추가하고,
`.claude/docs/` 문서의 신선도/정리 체계를 도입했다.

- **비대화형 init**: `connectbase init --yes --secret-key cb_sk_...` — 프롬프트 없이 진행.
  비TTY 환경에서 `--yes` 없이 실행하면 hang 대신 필요한 플래그를 안내하고 즉시 종료.
  `--app <앱ID>`/`--storage <스토리지ID>`/`--deploy-dir <dir>` 로 명시 지정, `--yes` 는
  프로젝트 이름과 같은 앱/스토리지가 있으면 재사용(반복 실행에 멱등). Secret Key 는
  `CONNECTBASE_SECRET_KEY` 환경변수로도 전달 가능.
- **Public Key 스프롤 방지**: `.connectbaserc` 에 `appId` 를 저장하고, 같은 앱을 다시 init
  하면 기존 Public Key 를 재사용 (이전에는 실행마다 새 키 발급).
- **문서 생성 스탬프 + 매니페스트**: `.claude/docs/` 파일에 생성 CLI 버전/날짜 스탬프를 넣고
  `.connectbase-manifest.json` 으로 추적 — 콘텐츠 해시로 신선도 비교, 서버에서 사라진 문서
  자동 정리(레거시 fullstack 파일 포함), `deploy` 성공 후 변경이 있을 때만 조용히 자동 갱신
  (`--skip-docs` 로 생략 가능).
- **CLAUDE.md @import**: 참조 블록이 `@.claude/docs/project-rules.md` import 로 규칙을 매
  세션 자동 로드. MCP 미연결 시 `npx connectbase mcp` 안내 지시 포함. `mcp` 명령도 비대화형
  지원(`--secret-key`/env/.connectbaserc 자동 사용).

### Changed / Security

- **키를 쿼리스트링에 싣지 않음**: 문서 다운로드가 `public_key` 쿼리 파라미터를 더 이상
  전송하지 않는다 (서버 측 키 치환은 플레이스홀더 불일치로 오래전부터 무동작 — 액세스 로그에
  키가 남는 위험만 있었음). `update` 의 secretKey fallback 도 제거.
- 모든 CLI 요청에 `User-Agent: connectbase-cli/<version>` 명시.
- `detectBuildDir`: Next.js 는 `output: 'export'` 미설정 시 경고 안내.
- 버전 비교가 prerelease(`-beta.1`) 형식에서 NaN 이 되던 문제 수정.
- `.gitignore` 항목 추가가 부분 문자열 오탐으로 건너뛰던 문제 수정 (라인 단위 정확 매칭).

## [3.43.0] - 2026-07-02

### Added — `--dev` 배포도 대용량 바이너리 지원

dev 배포(`connectbase deploy --dev`)는 DB 를 거치지 않고 파일을 dev Object Storage 경로에 직접
올리므로 6 MiB(TiDB 엔트리) 한도가 애초에 없다. 3.41.0~3.42.0 은 dev 에서도 fail-fast 로 대용량을
막았는데(과도한 제한), 이를 풀고 prod 와 동일하게 처리한다.

- 대용량 **바이너리**는 presign(`dev:true`)으로 dev 서빙 경로(`{id}/dev{path}`)에 직접 업로드하고 바디에서 제외(효율).
- 대응 core-server 가 없으면(구버전) 오프로드 없이 전량 base64 바디로 전송 — dev 는 DB 한도가 없어 안전.
- dev 는 이제 대용량 파일에 대해 fail-fast 하지 않는다.

## [3.42.0] - 2026-07-02

### Added — 웹 배포 대용량 바이너리 Object Storage 직접 업로드 (플랫폼 이슈 019f221b)

3.41.0 의 fail-fast 를 이어, 한도를 넘는 **바이너리**를 배포할 수 있게 했다. 파일당 6 MiB(TiDB
단일 엔트리 한도)를 넘는 바이너리(3D 모델·큰 동영상/폰트 등)를 배포하지 못하던 문제 해결.

- **대용량 바이너리 직접 업로드**: 한도를 넘는 바이너리는 presigned URL 로 **Object Storage 의 서빙
  경로에 직접** 업로드하고, 배포 요청에는 `pre_uploaded` 메타데이터만 전송한다. web-proxy 가 서빙하는
  바로 그 경로에 안착하므로 동일 origin·참조 재작성/CORS 불필요. (대응 core-server 필요. 미지원 서버면
  3.41.0 과 동일하게 fail-fast.)
- **fail-fast 유지**: 오프로드 불가한 파일(대용량 **텍스트**, 또는 서버 미지원)은 업로드 전에 즉시 중단.
- **3D 에셋 확장자 추가**: `.glb`/`.bin`(바이너리), `.gltf`(JSON)을 허용 확장자에 추가 +
  정확한 MIME(`model/gltf-binary` 등). 이제 3D 모델을 확장자 위장 없이 번들에 그대로 둘 수 있다.

## [3.41.0] - 2026-07-02

### Added — 웹 배포 파일당 크기 한도 사전 검증 (fail-fast)

`connectbase deploy` 가 파일을 수집한 직후, 서버로 업로드하기 **전에** 각 파일이 웹 스토리지
파일당 크기 한도를 넘는지 검사한다(플랫폼 이슈 019f221b). 기존에는 전량 수집·업로드를 마친
뒤에야 서버에서 원시 DB 오류(`Error 8025 (HY000): entry too large`)로 실패해 원인 파악이 어려웠다.

- 초과 파일이 있으면 어떤 파일이 얼마나 큰지(바이너리는 원본→base64 크기까지) 나열하고 즉시 종료한다.
- 검사 기준: 파일 내용은 DB(TiDB Text 컬럼)에 저장되며 단일 엔트리 한도 6 MiB 를 넘을 수 없다.
  바이너리는 base64(약 1.33x)로 저장되므로 원본 기준 실질 상한 약 4.5 MB. 텍스트는 UTF-8 바이트로 정확히 계산.

## [3.40.0] - 2026-06-28

### Added — KB 하이브리드(시맨틱) 검색 지원

지식베이스 검색에 BM25 키워드 + 벡터 의미 검색을 RRF(Reciprocal Rank Fusion)로 융합하는
하이브리드 검색을 추가했다. 콘솔 > **AI** 에서 임베딩을 켠 앱(gemini / openai BYOK)에서
동작하며, 오타·동의어·의역에 강하다.

- **\`KnowledgeSearchRequest.hybrid?: boolean\`** — \`cb.knowledge.search()\` 에 hybrid 옵션 추가.
  미설정(기본)이면 임베딩을 켠 앱에서 자동 적용, \`false\` 면 키워드 전용 검색을 강제.
- **\`AIChatRequest.hybrid?: boolean\`** — \`cb.ai.chat()\` / \`cb.ai.chatStream()\` 의 RAG 검색에도
  동일 옵션 적용. 미설정이면 임베딩을 켠 앱에서 자동 적용된다.
- 임베딩 미설정 앱에서는 무시되며 기존 BM25 검색으로 동작한다(하위호환, 회귀 없음).

## [3.39.2] - 2026-06-26

### Performance — 터널 프록시 응답 지연 제거 (TCP_NODELAY / keep-alive)

`connectbase tunnel` 프록시 경유가 직접 호출 대비 **~2.4초 고정 지연**을 추가하던 문제를
수정했다(플랫폼 이슈 019f01c3 — 실시간 TTS 음성에서 "첫 음성까지 ~3.5초"). 원인은 응답
본문을 tunnel-server 로 되돌리는 영구 WebSocket 소켓에 Nagle 알고리즘이 켜져 있던 것이다.

- **TCP_NODELAY (핵심)**: tunnel-server 로의 영구 WS 소켓에 `setNoDelay(true)` 적용. 프록시
  응답 본문은 CLI→tunnel-server 방향으로 CLI 가 송신하는데, 업스트림(사이드카)이 본문을
  여러 작은 청크로 흘리면 각 WS 프레임 write 가 직전 세그먼트의 ACK 를 기다리며 묶였다.
  tunnel-server 가 지리적으로 먼 경우(예: 한국↔싱가포르 ~45ms RTT) 이 Nagle×delayed-ACK
  상호작용이 프레임당 수십 ms 스톨을 만들고, 청크 수에 비례해 고정 수초로 누적됐다.
- **로컬 keep-alive**: 사이드카(로컬 업스트림) HTTP 호출에 keep-alive Agent 적용 — 요청마다
  TCP 핸드셰이크를 새로 맺지 않고 소켓을 재사용한다(순차 호출 시 이득).
- **로컬·업스트림 WS 소켓 NoDelay**: 사이드카 HTTP/WS 소켓에도 `setNoDelay(true)` 적용
  (인터랙티브 WS — noVNC·실시간 프레임 — 프레임당 지연 감소).

연계: core-server 도 프록시 forward 를 공개 도메인(`tunnel.connectbase.world`, 클러스터
밖 ingress 헤어핀)이 아니라 tunnel-server 내부 ClusterIP 로 직결하도록 함께 수정됐다.

## [3.39.1] - 2026-06-26

### Fixed — 터널 인증 4xx 를 재시도하지 않고 즉시 원인 표시

`connectbase tunnel` 이 인증 단계에서 받은 **클라이언트 오류(4xx)** 를 일시 장애처럼 10회
재시도하던 것을 수정했다. 서버가 4xx 로 명확히 거부하면 재시도 없이 즉시 원인을 출력하고 종료한다.

- **403 (앱 접근 거부)**: `app_id` 오설정(공개키 UUID·storageId 입력)이 대부분 — 서버 메시지와
  함께 "`app_id` 가 콘솔의 앱/프로젝트 ID 인지 확인" 안내를 출력하고 즉시 종료.
- **그 외 4xx (400/404 등)**: 설정 문제라 재시도해도 동일하게 실패 → 즉시 종료.
- **5xx · 429 · 네트워크 오류**: 기존대로 지수 백오프 재시도(일시적 장애).
- 서버 응답 본문의 `{"error":"..."}` 메시지를 파싱해 표시.

연계: tunnel-server 가 core 인증 4xx(403/401)를 503 "auth service unavailable" 로 뭉개던
서버측 문제도 함께 수정됐다(설정 오류가 서비스 장애로 오진단되던 것 해소).

## [3.39.0] - 2026-06-25

### Removed (BREAKING) — 게스트/익명 로그인 제거

게스트 멤버 로그인과 익명 플랫폼 로그인을 제거했다. 웹 클라이언트 코드 실수(예: 렌더마다
게스트 로그인 호출)로 게스트 멤버가 대량 생성되는 문제를 차단하기 위함이다.

- **제거**: `cb.auth.signInAsGuestMember()`, `cb.auth.clearGuestMemberTokens()`,
  `SignInAsGuestMemberOptions`, `GuestMemberSignInResponse`, `GuestSessionConflictError`.
- **제거**: `AuthSettingsResponse.allow_guest_login` 필드 (백엔드 `/v1/public/auth-settings`
  가 더 이상 반환하지 않음). 백엔드 엔드포인트 `POST /v1/public/app-members`(게스트 생성),
  `POST /v1/auth/signin/anonymous`(익명) 도 함께 제거됐다.
- **보존**: 기존 게스트 멤버 데이터는 그대로 유지되며, `getMe()` 의 `is_guest` /
  `auth_provider` 로 계속 식별 가능하다.
- **마이그레이션**: 비로그인/익명 사용자가 필요하면 정식 회원가입(`signUpMember`) 또는 소셜
  로그인을 사용할 것.

## [3.38.0] - 2026-06-25

### Added — `cb.push.sendToTopic` (토픽 구독자 전체 발송, service_role 지원)

토픽 구독자 전체에게 푸시를 보내는 `push.sendToTopic(appId, topicName, payload)` 를 추가했다.
`sendToMembers`(3.36.0)와 동일한 위임 가드(`hasPublicKey() && !hasJWT()`)를 적용해, 콘솔 JWT /
User Secret Key(`cb_sk_`) / service_role(`ctx.cbAdmin`) 인증으로 호출할 수 있고 순수 브라우저
Public Key(`cb_pk_`) 단독 인스턴스에서는 차단된다. 백엔드 라우트
`POST /v1/apps/:appID/push/send-to-topic` 는 이미 dual-auth(service_role 포함)를 지원한다.

- 대표 유스케이스: ConnectBase Function 스케줄러가 **시크릿 키 발급 없이** `ctx.cbAdmin` 으로
  정기 토픽 알림(일일 리마인더 등)을 발송. 콘솔에서 `cb_sk_` 를 만들어 함수 secret 에 넣던
  단계가 사라져 "MCP/Functions 만으로 끝까지" 자동화가 가능해진다.
- 새 타입 `SendToTopicPayload` 추가 (`SendToMembersPayload` 와 동일 옵션, 대상만 토픽).

```ts
await ctx.cbAdmin.push.sendToTopic(ctx.appId, 'daily-0900', {
    title: '오늘의 학습 리마인더',
    body: '잊지 말고 학습을 이어가세요!',
    data: { route: '/today' },
})
```

## [3.37.0] - 2026-06-25

### Changed (Breaking) — `cb.video.sendSuperChat` 시그니처 변경

슈퍼챗 전송이 백엔드 계약(`POST /v1/public/super-chats`, `channel_id` 필수)에 맞춰 정렬됐다.

- **이전**: `sendSuperChat(videoId, amount, message?, currency?)` → `SuperChat`
- **이후**: `sendSuperChat(channelId, amount, options?: { videoId?, liveId?, type?, message?, currency?, stickerId? })` → `SendSuperChatResponse`
- 대상은 `videoId` 또는 `liveId` 로 지정한다. 응답의 `client_secret` 으로 결제(Stripe PaymentIntent)를 확정해야 슈퍼챗이 노출된다.
- 새 타입 `SendSuperChatOptions`, `SendSuperChatResponse`, `SuperChatType` export.

마이그레이션: `cb.video.sendSuperChat('video-id', 5000, '메시지')` → `cb.video.sendSuperChat('channel-id', 5000, { videoId: 'video-id', message: '메시지' })`.

### Fixed — 비디오 메서드 백엔드 정렬 (이전엔 404/우회 필요했던 것들)

백엔드 구현·라우트 정렬로 다음 메서드가 실제로 동작한다 (채널/시청/좋아요취소/스토리지컨테이너는 SDK 시그니처 동일, 플레이리스트는 경로만 백엔드와 일치하도록 수정):

- **플레이리스트**: `createPlaylist` / `getPlaylists` / `getPlaylistItems` 경로를 백엔드 계약에 맞춤
  (`POST /playlists` body `channel_id`, `GET /playlists/public?channel_id=`, `GET /playlists/:id` 의 `items`).
- **채널 CRUD**: `createChannel` / `getChannel` / `getChannelByHandle` / `updateChannel` (백엔드 라우트 신규 구현).
- **시청 기록**: `reportWatchProgress` / `getWatchHistory` / `clearWatchHistory`.
- **좋아요 취소**: `unlikeVideo` (멱등 DELETE) — 빈 body 의 `likeVideo` 도 정상 동작하도록 백엔드 수정.
- **스토리지 컨테이너**: `storage.create` / `list` / `get` / `update` / `delete`.

## [3.36.0] - 2026-06-24

### Added — Functions 에서 시크릿 키 없이 푸시 발송 (service_role / act-as-member)

서버사이드 함수가 별도 시크릿 키(`cb_sk_`) 발급 없이 `ctx.cbAdmin`(service_role) 또는
`ctx.cb`(act-as-member)로 푸시를 보낼 수 있도록 `push.sendToMembers` 의 호출 가드를 완화했다.
백엔드 dual-auth(`POST /v1/apps/:appID/push/send`)가 service_role 토큰을 받도록 함께 확장됐다.

- **`push.sendToMembers` 위임 Bearer 허용** — 기존 가드 `if (hasPublicKey()) throw` 는
  `cbWithToken`(ctx.cbAdmin/ctx.cb) 클라이언트가 앱 식별용 `cb_pk_` 를 함께 보내는 탓에
  위임 호출까지 차단했다. 가드를 **`hasPublicKey() && !hasJWT()`** 로 변경해, 순수 브라우저
  (Public Key 단독) 인스턴스만 차단하고 위임 Bearer 토큰을 가진 인스턴스는 통과시킨다.
  공개 API 시그니처·타입 표면은 동일하다.

```ts
// service_role: true 함수에서 — 시크릿 키 불필요
await ctx.cbAdmin.push.sendToMembers(ctx.appId, [memberId], { title, body, data })
```

## [3.35.2] - 2026-06-22

### Changed — 문서·타입 폴리시 (런타임 동작 변화 없음)

SDK 감사에서 발견된 문서·타입 정합성 개선. 공개 API 의 런타임 동작은 동일하다.

- **README AI Streaming 멀티프로바이더 정정** — "Gemini 전용" 프레이밍을 제거하고 실제 지원 프로바이더 6종(`gemini` / `openai` / `claude` / `ollama` / `lm_studio` / `openai_compatible`)을 반영. Stream Options 표의 `provider` 타입을 전체 union 으로 교체하고, `provider`/`model`/`temperature` 의 기본값은 SDK 가 강제하지 않고 서버측 앱 AI 설정에서 결정됨을 명시. 누락돼 있던 `mcpGroup` 옵션도 표에 추가.
- **`endpoint.pollUntil` predicate 타입 강화 (타입 전용)** — predicate 파라미터를 `(body: any, ...)` → **`(body: unknown, ...)`** 로 변경. 공개 표면에 남아 있던 유일한 `any` 를 제거하고 호출자에게 응답 narrowing 을 강제한다. 런타임 동작은 동일하며, JSDoc 예제도 `unknown` narrowing 방식으로 갱신.
- **JSDoc 정정** — `recent-calls.ts` 의 breadcrumb 저장 정책 주석이 "쿼리 키 선별 redact" 라 적혀 있었으나 실제 구현은 쿼리스트링을 통째로 strip(더 안전)하므로 주석을 구현에 맞게 정정. `http.ts` 의 stale 참조 `client.support.getRecentCalls()` → `client.support.getRecentApiCalls()` 정정.

## [3.35.1] - 2026-06-19

### Changed — 내부 하드닝 (공개 API 동작 변화 없음)

프론트엔드 감사에서 발견된 SDK 내부 타입·안전성 개선. 공개 API 시그니처와 런타임 동작은 동일하다.

- **`HttpClient.getAppId()` 공개 접근자 추가** — `database` API 의 `requireAppId()` 가 `(this.http as unknown as { config })` 로 private 필드를 침투하던 cast 를 제거하고 정식 접근자를 사용.
- **토큰 응답 검증 가드 추가** — `signUpMember`/게스트 재발급/신규 게스트 로그인의 `setTokens` 호출 전에 `assertShape({ access_token, refresh_token, ... })` 검증을 추가(기존 `signInMember`/`getMe` 와 동일 패턴). 정상 응답은 그대로 동작하며, 서버가 잘못된 토큰 응답을 줄 때만 조용히 잘못된 토큰을 저장하지 않고 throw 한다.
- **타입 안전성** — analytics 의 `(window as any)`/`(navigator as any)` 를 좁은 인터페이스(`VisitorTrackerBridge`/`NavigatorWithGPC`)로 교체. 런타임 동작 무변화.

## [3.35.0] - 2026-06-17

### Fixed — doc-audit 발견 SDK/서버 계약 버그 (퍼블리시 전엔 미동작이던 기능 정상화)

3-way 문서 정합성 감사 중 발견된 SDK↔서버 계약 불일치를 코드 기준으로 정렬.

- **video 업로드 전면 정상화** — SDK↔서버 계약 불일치로 업로드가 동작하지 않던 것을 수정.
  - **Breaking (타입)**: `InitUploadResponse` 가 `{ session_id, video_id, ... }` → **`{ upload_id, chunk_size, total_chunks, expires_at }`** (서버 실제 응답).
  - 청크 업로드는 `PUT .../uploads/:uploadId/chunk?chunk_index=N`, 완료 응답 `{ video_id, status }` → `get(video_id)` 로 전체 Video 조회.
  - `getThumbnails()` 를 서버 단일 thumbnail 엔드포인트(`/thumbnail`)에 정합.
- **WebRTC 응답 타입 정합** — `getStats()`/`getRooms()` 타입이 서버 wire shape 와 달랐음.
  - **Breaking (타입)**: `AppStatsResponse` = `{ room_count, total_peers, total_broadcasters, total_viewers, channel_stats }`; rooms 는 `{ rooms: RoomSummary[] }` (`RoomStats` 제거, `ChannelStats`/`RoomSummary` 추가).
- **`subscription.cancel()`** — `immediate` 옵션이 서버에 전달되지 않던 것을 `cancel_at_end` 로 매핑. 인자 없이 호출 시 기본 = 현재 결제 주기 종료 시 해지.
- **`game.getRoom()`** — 서버 `{ room: ... }` 래핑을 언랩하지 않아 `undefined` 를 반환하던 것 수정.
- **HTTP `PUT`** — `FormData` 본문일 때 `Content-Type` 을 제거하지 않아 storage video 프록시 청크 업로드가 깨지던 것 수정.
- analytics JSDoc 예제의 존재하지 않는 `track()` → `trackEvent()` 정정.

> 모두 pre-launch(사용자 0) 단계 수정으로 실사용 호환성 파괴는 없습니다. video/WebRTC 타입 Breaking 은 직전까지 해당 기능이 end-to-end 로 동작한 적이 없습니다.

## [3.34.0] - 2026-06-15

### Changed — `geoQuery` 계약 표준화 (이전엔 백엔드와 형식이 달라 동작하지 않던 기능)

`geoQuery` 의 요청 형식이 백엔드 표준 계약과 달라 실제로는 동작하지 않았다(좌표·연산자
키 불일치). 단일 표준 계약으로 정렬하여 정상 동작하도록 수정.

- **Breaking (타입)**: `GeoPoint` 가 `{ latitude, longitude }` → **`{ lat, lng }`** 로 변경.
  서버는 `{ latitude, longitude }` 와 `[lng, lat]` 도 계속 허용하지만 SDK 타입 표준은 `{ lat, lng }`.
- **Breaking (타입)**: `GeoQuery` 의 `within` → **`box`** 로 변경 (`{ box: { bottom_left, top_right } }`).
  `near` / `polygon` 은 그대로.
- 반경 검색은 거리(미터) 가까운 순 정렬, 결과는 `{ results: [{ id, data, distance }], total_count }`.
- 영역이 지나치게 넓어 후보가 과도하면 결과를 임의로 자르지 않고 명시적 에러를 반환한다.

> 직전까지 `geoQuery` 는 end-to-end 로 동작한 적이 없어 실사용 호환성 파괴는 없습니다.

## [3.33.0] - 2026-06-15

### Added — Realtime WS 자동 SSE fallback (WS 가 차단된 네트워크에서도 AI 스트리밍 동작)

일부 가정용 ISP / 기업 프록시 / 미들박스는 HTTPS 는 통과시키면서 **WebSocket upgrade 만**
차단한다. 이 경우 `cb.realtime.connect()` 가 영영 `connected` 에 도달하지 못해 reject 되고,
이어지는 `cb.realtime.stream()` 이 `"Not connected. Call connect() first."` 로 실패했다
(platform-issue 019eb929 — 특정 end-user 만 WS 연결 실패, 같은 앱/키로 다른 환경은 정상).

- **변경**: `connect()` 가 WS 핸드셰이크에 실패해도 (자격증명이 있으면) reject 하지 않고
  **SSE degraded 모드로 resolve** 한다. 이후 `stream()` 호출은 자동으로 HTTP SSE
  (`/v1/public/ai/chat/stream`) 로 흘러 AI 스트리밍이 그대로 동작한다 — **앱 코드 변경 불필요**.
  Socket.IO/Pusher/Ably 와 동일한 transport graceful-degradation 패턴.
- **신규 옵션** `RealtimeConnectOptions.fallback?: 'sse' | 'none'` (기본 `'sse'`). `'none'` 이면
  기존처럼 WS 실패 시 `connect()` 가 reject 한다.
- **신규 getter** `cb.realtime.transport` (`'ws' | 'sse' | null`) — 현재 활성 transport 확인용.
- **경계**: SSE 는 단방향이라 fallback 모드에서는 AI `stream()` 만 지원된다. presence/typing/
  subscribe/sendMessage 등 양방향 기능은 SSE 모드에서 명확한 에러를 던진다. 또한 SSE 스트림은
  서버가 토큰 카운트를 별도 이벤트로 주지 않으므로 `onDone` 의 `totalTokens`/`promptTokens` 는
  0 으로 채워진다(`fullText`/`duration` 은 정확). `mcpGroup`(MCP 도구) 은 SSE fallback 미지원.
- **자격증명 없음**: public key/access token 이 아예 없으면(설정 오류) fallback 기본값이라도
  `connect()` 는 그대로 reject 한다 — SSE 로도 실패할 요청이므로 실제 버그를 묻지 않는다.

> 이미 배포된 앱도 npm `latest` / CDN UMD 번들 갱신 시 코드 수정 없이 자동 복구됩니다.

## [3.32.0] - 2026-06-09

### Fixed — `/v1/public/*` 요청에 무관한 콘솔 쿠키 첨부로 인한 401 / CORS 붕괴

`*.web.connectbase.world` 같은 ConnectBase 서브도메인에 배포된 앱에서, **ConnectBase 콘솔에
로그인된 브라우저**로 그 사이트를 보면 public API 호출(AI 챗 스트림, 함수 invoke, 테이블 조회,
에러 트래커 배치)이 전부 401 로 실패하던 회귀를 차단한다 (platform-issue 019ea60d).

- **근본 원인**: SDK 의 모든 fetch 가 `credentials:'include'` 로 나갔다. 앱이 ConnectBase
  서브도메인에 있으면 브라우저가 부모 도메인 `.connectbase.world` 의 **무관한 콘솔 세션 쿠키**까지
  자동 첨부한다. publicKey(`X-Public-Key`)로 인증하는 `/v1/public/*` 엔드포인트에 그 콘솔 User
  JWT 쿠키가 붙으면 401 이 나고, credentialed 요청에 `Access-Control-Allow-Origin: *` 가 충돌해
  브라우저가 응답 읽기까지 CORS 로 차단됐다. `autoRestoreSession:false` 로도 회피 불가(쿠키 첨부는
  브라우저 동작이라 앱 코드로 못 막음).
- **변경**: `/v1/public/*` 요청은 `credentials:'omit'` 으로 보내 쿠키 자체를 첨부하지 않는다.
  이 엔드포인트들은 헤더로만 인증하므로 쿠키가 불필요하다. 멤버 세션을 발급/회전하는
  `/v1/auth/*`(re-issue 등)는 HttpOnly refresh cookie 가 필요하므로 `'include'` 를 유지한다.

> 이미 배포된 앱도 npm `latest` / CDN UMD 번들 갱신 시 코드 수정 없이 복구됩니다.

### Added — `AIChatRequest.toolResultMaxChars` / `mcpPublicKey` 노출

게이트웨이는 지원하지만 SDK 타입에 빠져 있던 두 `chat`/`chatStream` 파라미터를 노출한다.

- **`toolResultMaxChars?: number`**: 서버측 도구 그룹 실행(`toolGroupId`)에서 각 도구 결과를 모델에
  **재투입하기 전** N 자(rune)로 잘라낸다(표시값 `tool_end.result` 는 원문 유지). 거대한 도구
  결과(예: `browser_snapshot`)가 컨텍스트를 부풀려 prefill 지연을 유발할 때 압축용
  (platform-issue 019ea60f, 게이트웨이는 019e8b80 에서 이미 출시됨).
- **`mcpPublicKey?: string`**: 요청에 포함한 MCP 도구를 호출할 외부 MCP 서버의 Public Key.

## [3.31.0] - 2026-06-05

### Added — OAuth 리다이렉트 콜백 **부팅 자동 소비** 안전망

소셜 로그인(구글 등) 리다이렉트 후 "로그인했는데 자꾸 로그인 페이지로 되돌아오는" 무한 루프를
SDK 레벨에서 차단한다. 여러 사용자가 동시에 보고한 회귀의 근본 안전망.

- **근본 원인**: 리다이렉트 방식은 콜백 URL(`?access_token=&refresh_token=&member_id=`)에서
  앱이 `cb.oauth.getCallbackResult()` 를 호출해야 토큰이 적재되고 cookie 가 부트스트랩된다.
  그런데 정적 호스팅(웹 스토리지)은 SPA fallback 으로 `/auth/callback` 같은 경로에 홈
  `index.html` 을 서빙하므로, 앱이 콜백 처리 코드를 빠뜨리면 URL 토큰이 **영영 소비되지 않아**
  세션이 확립되지 않고 인증 가드가 다시 로그인으로 되돌린다.
- **변경**: `new ConnectBase()` 생성자가 토큰-in-URL 리다이렉트 콜백(팝업 제외)을 감지하면
  `oauth.consumeRedirectCallbackOnBoot()` 를 boot-restore promise 로 등록해 **자동으로 토큰을
  적재 + cookie 부트스트랩**한다. 앱이 `getCallbackResult()` 를 호출하지 않아도 세션이 확립된다.
  `prepareHeaders` 가 이 promise 를 await 하므로 첫 `getMe()`(인증 가드 등)가 세션 확립 후 발화.
- **하위호환·안전성**: 앱이 이후 `getCallbackResult()` 를 호출해도 같은 promise 를 공유해 **이중
  bootstrap/rotation 을 방지**(boot 소비 실패 시에만 직접 적재로 폴백). 팝업(`window.opener`)·
  code-only(`?code=`)·에러(`?error=`) 콜백은 제외 — 기존 동작 유지. `autoRestoreSession: false`
  면 자동 소비도 끈다. token rotation race(2026-05-16) 없음: 사전 re-issue 없이 URL 토큰을 직접
  소비(=getCallbackResult 리다이렉트 경로와 동일)하기 때문.

> 이미 배포된 앱도 CDN 번들(`connect-base.min.js`) / npm `latest` 갱신 시 코드 수정 없이 복구됩니다.

## [3.29.0] - 2026-06-02

### Added — `connectbase tunnel --token <고정값>` proxy_token 핀 (platform-issue 019e8623)

비-public 터널의 `proxy_token` 이 **재연결마다 회전**해, 그 토큰을 스냅샷 저장하는 통합
(외부 MCP `custom_header X-Proxy-Token`, 터널을 가리키는 웹훅 등)이 다음 재연결부터
`401 invalid or missing proxy token` 으로 깨지던 문제를 해결한다.

- **`cli.ts` — `tunnel --token <value>`** — proxy_token 을 고정값으로 핀. 재연결 핸드셰이크마다
  같은 값(`?proxy_token=`)을 보내 회전을 방지한다. 16~128자 `[A-Za-z0-9_-]` 검증(클라 선검사 +
  서버 `ValidateProxyToken` 동일 규칙). 미지정 시 기존 동작(매 연결 새 발급) 유지 — 하위호환.
- **CLI 라벨 재등록 idempotent** — 같은 라벨 + 같은 `tunnel_id` 재등록은 더 이상 409 경고를
  내지 않고 기존 바인딩을 그대로 유지한다(콘솔 수동 PATCH 안내 제거). 라벨이 *다른* tunnel_id 에
  묶여 있을 때만 충돌로 처리. 회전이 이미 일어나 깨진 경우의 즉시 복구용으로 백엔드에
  현재 토큰 조회 경로(MCP `get_tunnel_label_proxy_token` / REST `GET .../endpoint-proxy-token/:label`)도
  추가됐다.

## [3.28.1] - 2026-06-02

### Added — `RelationType` 에 `many-to-one` 추가 (platform-issue 019e83a0-c7d0)

`cb.database.createRelation` 의 `relation_type` 으로 `many-to-one`(외래키→부모 단일 객체
populate 의 정석)을 지정할 수 있게 됐다. 기존엔 문서/MCP 는 `many-to-one` 을 안내하면서도
SDK/REST 관계 API 는 `one-to-one | one-to-many | many-to-many` 3종만 허용해, 문서대로
`many-to-one` 을 쓰면 `400 유효하지 않은 관계 타입` + TS 타입 에러가 났다.

- **`types/database.ts` — `RelationType`** 에 `'many-to-one'` 추가(4종 동등). additive 변경이라
  하위호환. 백엔드(data-server)도 ent enum·populate(단일 객체) 동등 처리하도록 함께 수정됨.

### Fixed — `oauth.signIn` JSDoc 예시 정정

`createIfNotExists` 예시가 `callbackUrl` 자리에 클라이언트 인스턴스(`cb`)를 넘기는 오기였다.
실제 URL 문자열로 교정(런타임/타입 영향 없음, 문서만).

## [3.28.0] - 2026-06-02

### Added — `cb.ai.chatStream` 취소(AbortSignal) 지원 (platform-issue 019e82e1)

`chatStream(request, callbacks)` 에 진행 중인 스트림/서버측 agent tool loop 를 끊을 수단이
없어, "중단" 시 클라이언트 콜백만 무음 처리되고 SSE 연결과 서버 도구 루프는 계속 진행됐다
(토큰·도구 호출 비용 지속).

- **`api/ai.ts` — `chatStream(request, callbacks, options?)`** — 선택적 3번째 인자
  `options?: { signal?: AbortSignal }` 추가. `signal` 을 fetch 에 그대로 전달해 abort 시
  SSE 연결을 닫는다. 연결이 닫히면 서버의 요청 컨텍스트가 취소되어 agent tool loop 도 중단된다.
- **`types/ai.ts`** — `AIChatStreamCallbacks` 에 `onAbort?: () => void` 추가, `AIChatStreamOptions`
  신규 export. abort 시 `onError` 가 아니라 `onAbort` 가 호출되고 reader 가 해제된다.
- 하위호환: 기존 2-인자 호출은 그대로 동작.

### Added — `cb.oauth.signIn` 의 `createIfNotExists` 옵션 (platform-issue 019e83b5)

`signIn` 은 기존 회원만 로그인하고 미가입 사용자는 콜백에서 `error=account_not_found` 로
반려된다(의도된 설계 — silent auto-signup 회귀 차단). 로그인=가입 통합 버튼을 한 호출로
처리하려는 경우를 위해 옵션을 추가했다.

- **`api/oauth.ts` — `signIn(provider, callbackUrl, state?, options?)`** — 선택적 4번째 인자
  `options?: { createIfNotExists?: boolean }` 추가. `true` 면 `intent=signup` 으로 동작해
  미가입 사용자를 자동 생성한다([signUp] 과 동일, idempotent). 백엔드 변경 없는 순수 passthrough.
- 하위호환: 기존 호출은 그대로 `signin` 의도.

## [3.27.0] - 2026-05-28

### Fixed — `signInAsGuestMember` 의 silent session overwrite 차단 + 게스트 식별 필드 노출 (platform-issue 019e6c5d)

같은 origin + 같은 publicKey 에서 OAuth 로그인이 활성인 채 다른 페이지/탭/콘솔에서
`cb.auth.signInAsGuestMember()` 가 호출되면, 게스트 토큰이 메인 토큰 슬롯
(`cb_auth_tokens_<hash>`) 을 silent 로 덮어써 사용자가 자동 생성된 게스트로 둔갑되던
회귀(sisun 운영 보고, 2026-05-28). 클라이언트는 둔갑 사실을 결정적으로 식별할
신호가 없어 닉네임 휴리스틱이나 SDK 내부 storage key 컨벤션에 결합된 비공식 우회에
의존해야 했다.

- **`api/auth.ts` — `signInAsGuestMember(opts?)`** — 활성 비-게스트 세션이 감지되면
  새 `GuestSessionConflictError` 를 던지고 진행을 거부한다. 명시적으로 교체하려면
  `{ allowOverrideExistingSession: true }` 옵션을 전달해야 한다. 활성 게스트 토큰을
  그대로 재사용하는 정상 경로는 그대로 통과한다.
- **`types/error.ts` — `GuestSessionConflictError`** 신규 export. `code = 'GUEST_SESSION_CONFLICT'`.
  index 에서도 re-export.
- **`types/auth.ts` — `MemberInfoResponse`** 에 `is_guest?: boolean` + `auth_provider?: string`
  추가. `auth_provider` 값은 `'guest' | 'email' | 'username' | 'oauth_<provider>'` 컨벤션.
- **백엔드 동반 변경** — `core-server` 의 `GET /v1/public/app-members/me` 응답에 동일
  필드가 같은 의미로 추가된다 (`appmember.Edges.Identities` 우선순위 OAuth > email >
  username > guest).
- **회귀 가드 테스트** — `test/signin-as-guest-session-guard.test.ts` (5 케이스).

#### Behavior change 영향

- 가드는 default-on. 활성 OAuth/email 세션 위에서 옵션 없이 `signInAsGuestMember()`
  를 호출하던 코드는 이제 throw 된다. RLS 우회용 cron 처럼 의도적으로 게스트를
  강제 갱신해야 하는 경우 `{ allowOverrideExistingSession: true }` 를 명시할 것.
- 응답 필드 추가만 있는 변경이므로 `getMe()` 호출자는 영향 없음. `is_guest` 를
  활용하면 SDK 내부 storage key 컨벤션에 결합된 비공식 휴리스틱을 제거할 수 있다.

## [3.26.0] - 2026-05-28

### Security — LLM provider API key 클라이언트 하드코딩 금지 명시화

2026-05-28 vLLM API key 강화 작업 중 다수 정적 자산에 raw provider URL + API key 가
하드코딩된 안티 패턴이 발견되어, SDK 표면에서 보안 정책을 명시한다. 동작 변경은 없다.

- **`api/ai.ts`** — `AIAPI` 클래스 / `chat()` 메서드 JSDoc 에 보안 경고 + 안티 패턴/
  베스트프랙티스 예시 추가. 클라이언트는 user public/secret key 만 사용하고, LLM
  provider API key 는 콘솔 `AppAIConfig` 에 암호화 저장(서버 측만 복호화). `cb.ai.chat`
  / `cb.ai.chatStream` 만 호출하면 서버 프록시가 tunnel `proxy_token` 자동 주입.
  raw provider URL(`api.openai.com`, `tunnel.connectbase.world/<id>` 등) 직접 fetch 금지.
- **`docs/18-sdk-ai.md`** — "🔒 보안 정책 (필수)" 섹션 신설(안티 패턴 / 베스트프랙티스 /
  마이그레이션 절차). 영향 받은 앱은 cross_app_issue 로 개별 마이그레이션 알림 예정.

본 버전은 런타임 동작 변경이 없는 문서/주석 강화이지만, 안티 패턴 차단을 SDK 표면에서
명시화하는 정책 변경이므로 minor 로 올린다.

## [3.25.1] - 2026-05-27

### Fixed — 3.25.0 회귀 복구 + heartbeat 헬퍼 정리

3.25.0 이 NativeBridge / inApp 우회 / deploy timeout 을 추가하면서, 같은 시기 3.24.0 에 들어가 있던
incremental deploy(manifest 기반 +/− diff) 와 `signUp` / `startCentralOAuth` 흐름을 통째로 누락한 채
release 됐다. 본 패치는 두 라인을 머지해 3.24.0 의 동작을 회복하면서 3.25.0 의 신규 기능을 모두 유지한다.

- **CLI deploy** — `fullDeploy` 만 호출되던 회귀를 닫고 `incrementalDeploy` + `tryFetchManifest` + `computeDeployDiff` + `handleDeployResponse` 복원.
  - Dev 는 항상 전량(`/deploy/dev`), Prod 는 manifest 기반 `+upsert/-delete` 전송, 변경 0 이면 업로드 skip.
  - 409 revision conflict 자동 1회 재시도.
  - 두 경로 모두 `computeDeployTimeout(totalBytes, override)` 결과를 `makeRequest` 의 `timeoutMs` 로 전달.
  - `startUploadHeartbeat()` 헬퍼로 분리해 incremental / full 양쪽에서 TTY heartbeat 표시 (CI 환경은 무음).
  - `VERSION` 을 빌드된 `package.json` 에서 읽도록 복원 (`getPackageVersion()`).
- **OAuth** — `signUp(provider, callbackUrl, state)` 와 `startCentralOAuth(intent)` 가 다시 export.
  - `signIn` / `signUp` 모두 `startCentralOAuth` 를 거치며, 내부에서 ① NativeBridge 감지 ②
    3rd-party 인앱 브라우저(`detectInAppBrowser` + `escapeToExternalBrowser`) 처리 ③ 일반 웹 redirect
    순으로 분기. 3.25.0 에서 `signIn` 만 inApp 처리를 받던 비대칭을 해소.
  - `signInWithPopup` 도 같은 3-way 분기 + `options.intent` 유지.
- **Exports** — `detectInAppBrowser`, `escapeToExternalBrowser` 외에 3.24.0 의 `TokenPersistence` 타입과
  `GameError` 도 함께 re-export (3.25.0 에서 export 라인이 충돌 머지로 일부 빠져있던 회귀).

### Notes — 누락된 CHANGELOG 보강

3.24.0 / 3.25.0 은 CHANGELOG 엔트리 없이 publish 됐다. 향후 retro 엔트리를 보강할 수 있으나, 본 릴리스는
3.25.0 → 3.25.1 의 diff 만 다룬다.

## [3.23.0] - 2026-05-26

### Fixed — OAuth 표준 흐름 페이지 리로드 후 세션 유실 (platform-issue 019e638d)

문서의 OAuth redirect 표준 예제 그대로 따라가도 **콜백 → 메인 페이지** 전환 직후 `cb.auth.getMe()` 가 401 으로 떨어져 비개발자 사용자가 영원히 로그인 루프에 갇히던 회귀를 3개 race + 1개 충돌 가드로 동시에 닫는다.

**(1) Race A — callback fire-and-forget cookie 부트스트랩**
`getCallbackResult()` / `exchangeCodeFromCallback()` 가 `setTokens` 직후 `bootstrapRefreshCookie()` 를 fire-and-forget 으로 호출했다. 표준 예제처럼 `window.location.href='/'` 가 즉시 발화하면 fetch 가 abort 되어 `cb_member_refresh_token` cookie 가 발급되지 않았다. 모바일·느린 네트워크에서 특히 잘 깨졌다.
- **변경**: `getCallbackResult()` 시그니처를 동기 → `Promise` 로 변경 (BREAKING — prelaunch 단계라 backward-compat 가드 생략). `exchangeCodeFromCallback()` / popup 핸들러도 cookie 부트스트랩을 *await* 한 뒤에 resolve. 호출자가 `await cb.oauth.getCallbackResult()` 후 navigation 하면 cookie 가 안전하게 저장된 상태가 보장된다.

**(2) Race B — 메인 페이지 entry race**
ConnectBase 인스턴스 생성 시 `tryRestoreSessionFromCookie()` 가 fire-and-forget 으로 진행되는데, 사용자 코드의 첫 인증 호출(`getMe()` 등) 이 그 promise 보다 먼저 발화하면 메모리 토큰 빈 채로 401 받았다.
- **변경**: 부팅 시 시작된 복구 promise 를 HttpClient 에 등록해, `prepareHeaders` 가 인증 호출 직전에 1회 await. 표준 예제 무수정으로 race 가 닫힌다.

**(3) 401 자동 복구**
인증 호출이 401 받으면 cookie 기반 복구를 *한 번* 시도하고 retry. cookie 가 살아 있는 사용자의 첫 호출이 화면 401 으로 깨지지 않게 한다 (cookie 복구 실패 시엔 원본 401 그대로 throw — 무한 retry 차단).

**(4) Backend strict X-Public-Key dispatch (충돌 C)**
같은 `.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`) 가 더 이상 트리거되지 않는다.

### Breaking

- `cb.oauth.getCallbackResult()` 가 `Promise` 를 반환합니다. 호출자는 반드시 `await` 해야 합니다. 컴파일 타임에 잡히지만, 동기 호출 코드는 콜백 결과를 `Promise<...>` 로 받아 `result.error` 가 항상 undefined 로 보입니다.
  - **마이그레이션**: `const result = cb.oauth.getCallbackResult()` → `const result = await cb.oauth.getCallbackResult()`. React `useEffect` 안에서는 IIFE 패턴: `;(async () => { const result = await cb.oauth.getCallbackResult(); ... })()`.

### Tests

- `test/http-race-recovery.test.ts` — race B / 401 auto-recovery / skipAuth 보존 5 케이스
- `test/oauth-callback-cookie-bootstrap.test.ts` — `await` 시그니처 + bootstrap-before-result 보장 추가

## [3.22.1] - 2026-05-26

### Documentation

README 에 3.22.0 에서 추가된 공개 API 3종을 정식 섹션으로 문서화. 코드 동작 변경 없음.

- **Key Types** 섹션에 `Server-side admin context` 추가 — `new ConnectBase({ publicKey, secretKey })` 가 admin 헤더(`X-Public-Key` + `Authorization: Bearer cb_sk_*`) 를 첨부하고 서버 `OptionalAdminSecretKey` 미들웨어가 RLS 를 우회한다는 점을 명시.
- **Authentication** 섹션에 `cb.auth.adminUpdateMember(memberID, fields)` 예제 + `secretKey` 미설정 시 throw / self-update 거절 동작 + `role` 이 RLS `auth.role` 의 backing field 임을 명시.
- **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 응답 예제.

## [3.22.0] - 2026-05-25

### Added — 서버사이드 admin (cb_sk_) 권한으로 data CRUD + 멤버 role 관리 (platform-issue 019e5a04, 019e59ca)

#### `Authorization: Bearer cb_sk_*` 자동 첨부

`ConnectBase({ publicKey, secretKey })` 처럼 publicKey 와 secretKey 를 동시에 보유한
인스턴스는 모든 요청에 publicKey 는 `X-Public-Key` (앱 식별), secretKey 는
`Authorization: Bearer cb_sk_*` (권한 격상) 두 헤더를 함께 전송한다.

서버는 새 `OptionalAdminSecretKey` 미들웨어가 cb_sk_ 를 검증하고 user 가 해당 앱에
대한 권한을 가지는지 (UserAndAppRole) 확인한 뒤 RLS 우회 컨텍스트를 set 한다.
서버사이드 sync / 관리 스크립트가 RLS 규칙 `.write: false` 를 우회해 데이터 CRUD
(`updateData`, `deleteData`, `createData`, `queryData`, `getData`) 를 수행할 수 있다.

#### `cb.auth.adminUpdateMember(memberID, fields)` 추가

RLS 평가 변수 `auth.role` 의 backing field 인 `member.role` 을 set 할 수 있는 SDK
경로. self-update 가 아닌 admin 권한 호출이므로 secretKey 가 필수 (없으면 throw).
`{ nickname, role, custom_data }` 모두 PATCH 방식.

```typescript
const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' })
await cb.auth.adminUpdateMember('member-uuid', { role: 'admin' })
```

### Added — Function raw HTTP webhook 헬퍼 (platform-issue 019e597c)

함수가 `http_trigger_enabled=true` 로 생성/수정되면 외부 SaaS (Discord / Stripe /
GitHub / Slack Events / Notion) 가 raw HTTP webhook 으로 호출할 수 있다. body 는
wrap 없이 raw bytes 그대로 함수 핸들러에 전달 (Ed25519/HMAC 서명 검증 호환), 모든
헤더 forward, method/path/query 보존. 함수가 `{statusCode, headers, body}` 를
반환하면 그대로 HTTP 응답에 매핑.

```typescript
const url = cb.functions.getWebhookURL('function-id')
// → https://api.connectbase.world/v1/public/functions/<id>/webhook
// Discord Interactions Endpoint URL / Stripe webhook endpoint 에 직접 등록.
```

## [3.21.1] - 2026-05-24

### Fixed — `signInWithPopup` 콜백 cookie 부트스트랩 누락 회귀 (platform-issue 019e5922)

popup 콜백은 `*.connectbase.world` 중앙 도메인에서 발생하므로 부모 window 도메인
(예: `fream-note.com`) 에 `cb_member_refresh_token` cookie 가 직접 발급되지 않는다.
기존 `signInWithPopup` 메시지 핸들러는 `setTokens()` 만 호출하고
`bootstrapRefreshCookie()` 를 호출하지 않아, `persistence='none'` + 페이지 새로고침 시
메모리 토큰이 사라지고 같은 root domain 의 콘솔 cookie 가 cross-origin 으로 따라오는
환경에서는 `/v1/auth/re-issue` 가 fallback 으로 콘솔 User JWT 를 발급해 화면이 깨졌다.

다른 콜백 경로(`getCallbackResult` / `exchangeCodeFromCallback`) 와 동일하게
`bootstrapRefreshCookie()` 호출을 추가해 부모 window 도메인의 member cookie 를
명시적으로 부트스트랩한다. `await` 하지 않는 best-effort 호출이라 호출자 흐름은 변하지 않는다.

회귀 가드: `oauth-callback-cookie-bootstrap.test.ts` 에 `signInWithPopup` 케이스 추가.

## [3.21.0] - 2026-05-23

### Added — `CB_*_URL` 환경변수 폴백

ConnectBase 생성자가 `baseUrl` / `socketUrl` / `webrtcUrl` / `videoUrl` / `gameUrl` 을
결정할 때 우선순위가 ① config 명시값 → ② `CB_*_URL` 환경변수 → ③ 공개 기본값 순서로
적용된다.

용도: 클러스터 내부에서 실행되는 서버리스 함수가 외부 LB(공개 URL)로 hairpin 하지 않고
내부 service URL 로 직접 연결하도록. 함수 코드 변경 없이 function-server 가
`CB_BASE_URL` / `CB_SOCKET_URL` 등을 주입하는 것으로 동작.

브라우저 환경(process undefined) 에서는 env 가 항상 undefined — 기존 동작과 동일.

## [3.20.1] - 2026-05-22

### Fixed — `AgenticSearchProgress` 타입 export 누락

3.20.0 의 CHANGELOG 는 `AgenticSearchProgress` 를 신규 export 타입으로 명시했으나,
패키지 진입점(`src/index.ts`)의 export 구문에서 누락되어
`import type { AgenticSearchProgress } from 'connectbase-client'` 가 `TS2614` 로
실패했다. export 목록에 추가하여 `onSearching` 콜백을 타입 안전하게 쓸 수 있게 했다.

- 런타임/동작 변경 없음 — 타입 export 누락만 수정.
- 우회용으로 쓰던 `NonNullable<AIStreamChunk['searching']>` 추출은 더 이상 필요 없다.

## [3.20.0] - 2026-05-22

### Added — Agentic 검색 진행 실시간 중계 (`chatStream` 의 `onSearching`)

`cb.ai.chatStream({ agentic: true })` 시 agentic 다중검색의 진행 상황을 받는
`onSearching` 콜백을 추가했다. 검색어 생성(`query_generation`) → 검색 실행
(`searching`, 생성된 쿼리 포함) → 종료(`complete`, 라운드 수·결과 수)의 각 단계가
실시간으로 전달되어 "검색 중…" 진행 UI 를 구성할 수 있다.

- 신규 타입 `AgenticSearchProgress` export — `phase` / `round` / `queries` /
  `results` / `rounds`.
- 종료 이벤트(`phase: 'complete'`)의 `rounds` 로 agentic 이 실제 몇 라운드
  수행됐는지(0 = 폴백) 판별 가능 — `knowledge.search` 응답의 `agentic_rounds`
  와 동일한 신호.
- 기존 동작 영향 없음: agentic 미사용 시 `onSearching` 은 호출되지 않는다.

## [3.19.0] - 2026-05-22

### Added — AI 추론(reasoning) 스트리밍 (`chatStream` 의 `onReasoning`)

추론형 모델의 추론 과정을 스트리밍으로 받는 `onReasoning` 콜백을 `cb.ai.chatStream` 에
추가했다. 게이트웨이가 업스트림(openai 계열)의 `delta.reasoning` 을 토큰 청크의
`reasoning` 필드로 중계한다. 추론 델타는 본문(`content`)이 비어 있어 `onReasoning` 을
쓰지 않는 구형 코드에는 영향이 없다 (하위호환).

### Added — Agentic 검색 수행 신호 (`KnowledgeSearchResponse.agentic`)

`cb.knowledge.search` 응답에 `agentic` / `agentic_rounds` 필드를 추가했다. `agentic: true`
를 보냈더라도 AI provider 미설정·LLM 오류로 단일 키워드 검색에 폴백되면 `agentic` 이
`false` 로 와, agentic 옵션이 실제로 수행됐는지 폴백됐는지 판별할 수 있다.

### Note — npm 패키지 동기화

`onReasoning` 과 `knowledge.updateDocument`(아래 3.18.0 항목)는 코드에는 반영됐으나
npm 3.18.0 패키지에는 누락된 상태였다. 3.19.0 에 모두 정상 포함된다.

## [3.18.0] - 2026-05-21

### Added — Knowledge Base 문서 수정 (`knowledge.updateDocument`)

기존 KB 문서를 수정하는 `cb.knowledge.updateDocument(kbID, docID, data)` 메서드를 추가했다.
`content` / `file_content` / `metadata` 중 하나라도 포함하면 서버가 전체 재색인(기존 청크
삭제 → 재청킹 → 재색인)을 수행하며, `name` 만 보내면 재색인 없이 문서 라벨만 변경된다.
재색인 시 서버는 `status='pending'` 으로 즉시 응답하고 색인은 백그라운드로 진행한다
(`addDocument` 와 동일).

- 신규 타입 `UpdateDocumentRequest` export.
- REST `PUT /v1/public/knowledge-bases/:kbID/documents/:docID` 에 대응.

### Note — Knowledge Base 색인 과금

서버에서 KB 문서 색인이 사용량 과금 대상이 되었다. 문서 생성·수정(재색인) 시 색인 토큰이,
색인된 청크 보관 용량이 사용량으로 집계된다. SDK 사용 방식에는 변화가 없으며 BM25 키워드
검색 자체는 외부 임베딩 API 비용이 없다.

## [3.17.2] - 2026-05-20

### Fixed — 콘솔 도메인 세션 쿠키 누수로 인한 앱 공개 API 401

`*.web.connectbase.world` 서브도메인에 배포된 앱은 콘솔 도메인(`connectbase.world`)의
세션 쿠키를 `.connectbase.world` 스코프로 공유받는다. 로그인 기능이 없는 앱에서 개발자가
ConnectBase 콘솔에 로그인된 브라우저로 자기 앱을 열면, `autoRestoreSession` 이 `/v1/auth/re-issue`
로부터 콘솔 User JWT 를 복구해 앱 공개 API(`/v1/public/*`) 요청의 `Authorization` 헤더에
첨부했고, 서버가 이를 앱 멤버 토큰으로 검증하려다 실패해 **401** 이 발생했다. 일반 방문자에겐
재현되지 않고 개발자가 자기 앱을 테스트할 때만 터지는 형태였다 (platform-issue `019e459c`).

수정:
- `/v1/auth/re-issue` 가 콘솔/플랫폼 토큰(`role: User` / `AdminInvite`)을 돌려주면 세션으로
  채택하지 않는다 — 실패가 아닌 "앱 세션 없음" 으로 silent 처리.
- 메모리의 access token 이 콘솔/플랫폼 토큰이면 `Authorization` 헤더에 첨부하지 않는다 —
  `X-Public-Key` 공개 키 인증으로 폴백.
- 앱 세션 토큰(`AppMember` / `Guest`)과 디코드 불가한 토큰은 영향 없음 — 콘솔 토큰으로
  확정될 때만 거부하므로 정상 세션을 잘못 폐기하지 않는다.

로그인 기능이 없는 앱은 `autoRestoreSession: false` 로 명시하면 이 세션 복구 자체를 건너뛸 수 있다.

## [3.17.1] - 2026-05-19

### Docs — README 에 Knowledge Base (RAG) 섹션 신설

기존 README 의 API Reference 가 game / auth / database / storage / realtime / endpoint /
push / webrtc / payments / support 만 다루고 knowledge 섹션이 통째로 누락된 상태였음.
npm 페이지에서 SDK 사용자가 RAG 기능 존재를 인지하지 못하는 누락 보강.

추가:
- API Reference 에 "Knowledge Base (RAG)" 섹션 — `addDocument` (text/url) /
  `addDocumentFromFile` (PDF/DOCX/text, 50MB) / `listDocuments` / `deleteDocument` /
  `search` (BM25 + Agentic) / `searchGet` 예제
- 파일 업로드 제약 (지원/미지원 MIME, 50MB, OCR 미지원) 박스
- 사용자별 격리 (AppMember JWT 기반 metadata.user_id 자동 태깅) 안내
- Features 리스트에 "Knowledge Base (RAG)" 한 줄 추가

코드 변경 없음 — docs only. 코드 표면은 3.17.0 과 동일.

## [3.17.0] - 2026-05-18

### Added — `cb.knowledge.addDocumentFromFile` (PDF / DOCX / text 파일 업로드)

`KnowledgeAPI.addDocumentFromFile(kbID, file, options?)` 를 신설했다. 브라우저 `<input type="file">`
로 받은 `File` / `Blob` 또는 Node.js `Buffer` / `Uint8Array` 를 그대로 넘기면 SDK 가 base64 인코딩
+ MIME 추출 후 서버에 보낸다. 서버는 PDF / DOCX / text 류를 자동 텍스트 추출하여 기존 청킹·인덱싱
파이프라인을 태운다 (platform-issue `019e3a31`, ai-tool chat RAG 제보).

지원 MIME:
- `application/pdf` (텍스트 PDF — 스캔 이미지 PDF 는 미지원)
- `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (DOCX)
- `text/*` (plain / markdown / csv / html)
- `application/json`

파일 크기 상한: 50MB (원본 바이너리). 이미지 OCR / HWP / XLSX 는 향후 추가 예정 — 현재는 명시적 에러.

```ts
// 브라우저
const file = event.target.files[0]
const doc = await cb.knowledge.addDocumentFromFile('kb-id', file, {
    metadata: { tag: 'manual' },
})

// Node.js
import { readFileSync } from 'node:fs'
const doc = await cb.knowledge.addDocumentFromFile('kb-id', {
    data: readFileSync('./report.pdf'),
    mimeType: 'application/pdf',
    name: 'report.pdf',
})
```

`CreateDocumentRequest` 에 `file_content`(base64) / `mime_type` 옵션 필드를 추가해 기존 `addDocument`
도 동일 흐름을 직접 호출 가능. `source_type` enum 에 `'file'` 추가.

서버 측 변경 (core-server):
- `pkg/file_extractor` 신설 — PDF (ledongthuc/pdf, pure-Go) / DOCX (stdlib archive/zip + xml)
- `KnowledgeBaseService.createDocumentInternal` 가 source_type='file' 분기에서 base64 디코딩 →
  텍스트 추출 → 기존 청킹 파이프라인 진입
- MCP `add_kb_document` 도구도 `file_content` / `mime_type` 인자를 받아 SDK 와 동일한 표면 제공

회귀 가드:
- `test/knowledge-add-document-from-file.test.ts` — 6 케이스 (Blob / File-like / Uint8Array / metadata /
  default name / invalid input)
- backend `file_extractor` 10 케이스 + KB service `file_source_test.go` 5 케이스

영향 범위:
- 신규 메서드 추가 — 기존 호출자 코드 변경 불필요.
- `CreateDocumentRequest` 가 두 optional 필드 추가만 — 기존 사용 패턴 호환.

## [3.16.3] - 2026-05-18

### Fixed — `persistence='none'` + OAuth redirect callback 첫 새로고침 시 세션 풀림

`new ConnectBase({ persistence: 'none' })` + `cb.oauth.signIn(provider, redirectUri)` (redirect 방식)
로 로그인한 후 첫 새로고침에서 세션이 풀리는 회귀를 수정 (platform-issue `019e3960`, NJB 제보).

원인: OAuth 콜백의 302 응답에 동봉된 `Set-Cookie: cb_member_refresh_token=...` 가 일부 deployment /
브라우저 정책 / 사용자 설정 (3rd-party cookie 차단 등) 환경에서 브라우저에 저장되지 않는다.
`persistence='none'` 은 토큰을 메모리에만 두므로, 페이지 새로고침 후 SDK 가 `/v1/auth/re-issue` 를
cookie 없이 호출하게 되어 401 → `clearTokens` → 강제 로그아웃되었다.

수정:
- `OAuthAPI.getCallbackResult()` / `exchangeCodeFromCallback()` 가 토큰을 메모리에 적재한 직후
  `HttpClient.bootstrapRefreshCookie()` 를 fire-and-forget 으로 호출한다. 이 호출은
  `persistence === 'none'` 일 때만 `/v1/auth/re-issue` 를 Bearer 흐름으로 한 번 실행해 서버가
  HttpOnly cookie 를 명시적으로 발급하도록 유도한다.
- 백엔드 `/v1/auth/re-issue` 가 `Authorization: Bearer` + `X-Public-Key` 조합을 SDK 호출 신호로
  인식하면 응답에 `cb_member_refresh_token` cookie 를 함께 내려준다 (server-side 변경).

회귀 가드:
- `test/oauth-callback-cookie-bootstrap.test.ts` — 4 케이스 (getCallbackResult / exchangeCodeFromCallback /
  에러 응답 / 토큰 누락)
- `test/http-cookie-bootstrap.test.ts` — 7 케이스 (persistence 별 동작, 비-브라우저, 5xx/401 silent 실패)

영향 범위:
- `persistence='none'` (기본값) 으로 OAuth redirect callback 을 쓰던 모든 SDK 사용자.
  `persistence='localStorage'` / `'sessionStorage'` 사용자는 변경 사항 없음.
- 추가 1회 re-issue 호출 비용 외에 호출자 코드 변경 불필요.

## [3.16.2] - 2026-05-16

### Fixed — OAuth redirect 콜백 페이지에서 autoRestoreSession ↔ getCallbackResult rotation race

OAuth 리다이렉트 콜백 페이지(`?access_token=&refresh_token=` legacy, `?code=&state=` code-only,
`?error=&state=` 에러) 에서는 SDK 생성자가 `autoRestoreSession` 을 더 이상 발화시키지 않는다.
콜백 페이지에서 자동 복구가 발화하면 cookie 의 refresh token 이 즉시 rotation 되는데,
곧이어 앱 코드가 `cb.oauth.getCallbackResult()` / `exchangeCodeFromCallback()` 으로 URL 의
*원본* refresh token 을 `setTokens()` 해버리면 메모리는 revoked 된 원본을 들고 있게 된다.
이후 access 만료 시 SDK 가 Bearer 로 revoked token 을 보내 reuse-detection 에 걸려
401 + family 전체 revoke 로 사용자가 강제 로그아웃되는 회귀가 있었다 (platform-issue
2026-05-16, NJB).

콜백 페이지에서는 `getCallbackResult()` / `exchangeCodeFromCallback()` 가 토큰 적재를
책임지므로 자동 복구가 불필요하다. 일반 페이지(/, /play 등) 에서는 기존과 동일하게 발화한다.

회귀 가드: `test/auto-restore-session-callback-skip.test.ts` — 6 케이스
(일반 / legacy 토큰-in-URL / code-only / error / 단독 access_token / autoRestoreSession=false).

## [3.16.1] - 2026-05-16

### Added — `SCRIPT_SETUP_OVERRUN` GameErrorCode literal

game-server 의 hook ctx 2-phase split (setup vs hook deadline 분리, backend
commit `ffe5108a` + `2132af4f`) 에 맞춰 `GameErrorCode` literal union 에
`'SCRIPT_SETUP_OVERRUN'` 을 추가했다. 사용자 hook 본문이 아닌 SDK 가 매 호출
주입하는 setup 단계 (스크립트 로드 + state 직렬화) 가 budget 을 초과한 경우
구분 가능하도록 한 4채널 동등 작업의 JS/TS 측. Unity / Godot / Unreal 도 동일
시점에 동일 코드를 노출.

backward-compat: 신규 literal 추가만 — 기존 사용 패턴 호환.

## [3.16.0] - 2026-05-14

### Added — cross-app OAuth 로 Provider 함수 호출 / Database Realtime 구독

cross-app OAuth access token 으로 (1) Provider 앱의 ConnectBase Function 을 invoke 하고
(2) Provider 앱의 테이블을 Database Realtime 으로 구독하는 정식 경로를 추가했다.
이전에는 cross-app OAuth 토큰이 보호 리소스에 도달하는 경로가 issue queue 뿐이라,
발급받은 토큰으로 함수/Realtime 을 쓸 수 없었다 (platform issue `019e2645`).

- **`cb.functions.invokeCrossApp(providerAppId, functionId, accessToken, payload?, timeout?)`**
  — Provider 가 `function:invoke` scope 를 노출하면 Consumer 가 cross-app OAuth access token
  으로 Provider 의 함수를 실행한다. 호출자 신원은 함수 런타임 `ctx.memberId`(end-user) /
  `ctx.callerAppId`(Consumer 앱) 로 전달된다.
- **`cb.database.connectRealtime({ accessToken })`** — `accessToken` 이 cross-app OAuth
  access token 이면 Provider 앱(`database:read` scope 노출 시)의 테이블을 구독한다. 구독
  대상 앱은 토큰 `aud` 로 결정되므로 Provider 의 publicKey 가 불필요하다. RLS 는 토큰
  `end_user_id` 를 subject 로 평가한다 (member id 기반 규칙 정상 동작, email/role 기반
  규칙은 게스트로 fail-safe 평가).
- 함수 런타임 `ctx` 에 `memberId` / `callerAppId` 필드 추가 (nodejs/go/python/dotnet) —
  cross-app 호출이 아니면 빈 문자열.

## [3.15.0] - 2026-05-14

### Added — `GameEventHandlers.onMessage` 커스텀 broadcast 메시지 핸들러

게임 서버 Lua 의 `room.broadcast(data)` / `room.send_to(clientId, data)` 로 보낸,
SDK 가 모르는 `type` 의 커스텀 메시지를 받는 catch-all 핸들러를 추가했다.

이전에는 `GameRoom.handleMessage` 의 `switch` 가 `delta`/`state`/`player_event`/
`chat`/`error` 표준 타입만 처리하고 `default:` 에서 메시지를 **drop** 했다. 그래서
게임별 커스텀 프로토콜(예: `{ type: "chunk", ... }`, `{ type: "turn_played", ... }`)을
클라이언트가 받을 방법이 없었다 — starter template 의 client 코드가 동작 불가능한
상태였다.

- `room.on('onMessage', (msg) => { ... })` — 표준 타입(`delta`/`chat` 등)은 기존 전용
  핸들러로 가고, 그 외 커스텀 `type` 메시지만 `onMessage` 로 전달된다. `msg` 는
  `{ type: string } & Record<string, unknown>` 형태이며 `msg.type` 으로 분기한다.
- 미설정 시 기존과 동일하게 무시(drop) — 하위 호환 100%, 동작 변화 없음.

`examples/game-prototypes/*/client/connect.ts` 의 3개 starter template 이 이 핸들러로
재작성됐다 (`onMessage` 로 chunk / turn_played / character_moved 등 수신).

## [3.14.2] - 2026-05-14

### Fixed — `cb.game.createClient()` appId 누락 시 silent 연결 깨짐

NJB 측 `createRoom` → `SCRIPT_NOT_FOUND` 오탐 보고 (019e2210) 로 발견. `appId` 가
`new ConnectBase({ appId })` 에 설정되지 않은 상태에서 `cb.game.createClient()` 를 쓰면
`GameRoom.buildConnectionUrl` 이 `wss://.../v1/game//ws` (빈 path 세그먼트) 를 만들어
game-server 의 `conn.appID` 가 빈 문자열로 들어갔다. 그 결과 `createRoom` 의 스크립트
검증이 `GetMeta("", name)` → `nats: invalid key` 로 실패해 실제로 존재하는 active
스크립트가 `SCRIPT_NOT_FOUND` 로 오탐. HTTP 스크립트 메서드 (`uploadScript(appId, ...)`)
는 appId 가 명시적 인자라 영향 없었고, WS 클라이언트만 전역 appId 에 의존해 깨졌음.

- `GameRoom.connect()` — `appId` 가 비어 있으면 빈 URL 로 붙는 대신 명확한 에러로 즉시
  reject (`appId is required to connect ...`). `buildConnectionUrl` 에도 hard invariant
  가드 추가 — `/v1/game//ws` 는 더 이상 생성되지 않는다.
- `GameAPI.createClient({ appId })` — per-call `appId` 지정을 지원. 이전엔 `this.appId`
  (전역) 가 `config.appId` 를 무조건 덮어써서 호출별 지정이 불가능했다. 이제
  `config.appId ?? this.appId` 로 per-call 우선.

> game-server 측에도 defense-in-depth 보정 동반 — `public_key` / `token` 으로 인증된
> WS 연결은 auth 가 resolve 한 appID 를 authoritative 로 사용 (URL path 의 빈 `:appID`
> 무시). 유효한 publicKey 를 보내는 클라이언트는 SDK 업그레이드 없이도 자동 교정된다.

### Added — 회귀 가드 테스트

- `test/game-connect-appid.test.ts` — appId 누락 시 `connect()` reject + WS 미생성,
  `createClient({ appId })` per-call override, `/v1/game//ws` 절대 미생성 단언.

## [3.14.1] - 2026-05-14

### Fixed — `cb.game.*` primitive 호스트 라우팅 회귀 (v3.0.0 이후 잠복)

NJB 측 deleteScript / deactivateScript 404 보고 (2026-05-14) 로 발견. v3.0.0 BREAKING 에서
matchqueue / leaderboard / scripts primitive 메서드 16개가 `this.http.*` (기본 baseUrl
= core-server, `api.connectbase.world`) 를 거치도록 잘못 wired 되어 game-server
(`game.connectbase.world`) 로 라우팅되지 않고 404 회귀. NJB 는 v3.9.0 부터 `uploadScript`
회귀를 raw fetch 로 우회 중이었음 (apps/game/scripts/upload-lua.mjs 참고).

수정: `GameAPI` 내부 `gameFetch` private helper 도입. `${this.gameServerUrl}${path}` 기준
fetch + 표준 `getHeaders()` (X-Public-Key / Bearer) + 서버 응답 `{ error }` → `ApiError.code`
surface + 204/non-JSON 응답을 `undefined` 로 처리. 영향받은 메서드:

- **scripts (8)**: `uploadScript` / `listScripts` / `getScript` / `listScriptVersions` /
  `activateScript` / `rollbackScript` / `deactivateScript` / `deleteScript`
- **matchqueue (3)**: `enqueueMatch` / `listMatchqueue` / `cancelMatch`
- **leaderboard (6)**: `submitScore` / `getTopScores` / `getMemberRank` /
  `getRankAround` / `resetLeaderboard` / `removeFromLeaderboard`

기존에 raw fetch 로 우회 중이던 통합 코드는 SDK 본 메서드로 회귀해도 무방 (4xx/5xx 시
`ApiError` 로 throw 되며 status/code 분기는 동일하게 작동).

### Added — 회귀 가드 테스트

- `test/game-host-routing.test.ts` — 17개 메서드 전체가 `game.*` 호스트로 라우팅되는지
  fetch mock 으로 단언. 동일 회귀 재발 방지.

## [3.14.0] - 2026-05-14

### Added — `GameError` 클래스 + `createRoom` 응답 메타

- 신규 export: `GameError` (extends `Error`) — game-server 의 `error` 메시지를 일관된
  인스턴스로 surface. `.code`, `.phase`, `.feature`, `.originClientId`, `.requested`,
  `.available` 모두 노출. `instanceof GameError` 로 UI 분기 가능.
- 신규 export: `GameErrorCode` literal union — `'SCRIPT_NOT_FOUND' | 'NO_ACTION_HANDLER'
  | 'FEATURE_DISABLED' | 'SCRIPT_ERROR' | 'SCRIPT_TIMEOUT' | 'RATE_LIMITED' |
  'QUOTA_EXCEEDED' | 'TIMEOUT' | 'UNKNOWN' | (string & {})`.
- 신규 export: `CreateRoomResult` 타입 — `{ roomId, state, scriptName?, scriptVersion? }`.
- 신규 메서드: `GameRoom.createRoomDetailed(config)` + `GameRoomTransport.createRoomDetailed(config)`
  — server 가 attach 한 lua script 의 이름/버전을 client 가 즉시 검증할 수 있게 응답에 포함.
- 신규 getter: `room.scriptName` / `room.scriptVersion` (`GameRoom` + `GameRoomTransport`)
  — 마지막 `createRoom` 응답의 메타. `joinRoom`/`leaveRoom`/`disconnect` 시 reset.
- 기존 `createRoom(config)` 의 시그니처는 그대로 (호환). 내부적으로 `createRoomDetailed`
  를 호출하고 `.state` 만 반환.
- `onError` 콜백 시그니처가 `Event | ErrorMessage` → `Event | GameError` 로 변경 (호환:
  `ErrorMessage` 의 모든 필드가 `GameError` 인스턴스에 동일하게 surface 됨).

### Changed (BREAKING) — `disableScript` rename + `deleteScript` 신규

server 측 `DELETE /v1/game/:appID/scripts/:name` 의 의미가 **Disable → hard-delete** 로
변경된 것에 대응:

- `GameAPI.disableScript(appId, name)` **제거**. 대체: `GameAPI.deactivateScript(appId, name)`
  — `POST /scripts/:name/deactivate` 호출 (코드/버전 보존, 재활성화 가능).
- `GameAPI.deleteScript(appId, name)` **신규** — `DELETE /scripts/:name` 호출 (메타 + 모든
  버전 영구 제거, 복구 불가).

### Fixed — `error` 메시지 분류 메타 손실 회귀

이전엔 모든 `reject(new Error(msg.data.message))` 로 server 의 `code`/`phase`/`feature`/
`origin_client_id`/`requested`/`available` 가 손실되어 SDK 사용자가 UI 분기를 만들 수
없었다 (platform-issue 019e21dd, NJB 2026-05-13). 이제 `GameError` 인스턴스로 reject
되어 모든 메타가 보존된다.

### Migration

```ts
// Before
import { ErrorMessage } from 'connectbase-client'
room.on('error', (err) => {
    if ((err as ErrorMessage).code === 'FEATURE_DISABLED') { /* ... */ }
})
await cb.game.disableScript(appId, 'old-script')

// After
import { GameError } from 'connectbase-client'
room.on('error', (err) => {
    if (err instanceof GameError) {
        if (err.code === 'SCRIPT_NOT_FOUND') console.error('candidates:', err.available)
        if (err.code === 'NO_ACTION_HANDLER') console.error('handler for', err.phase, 'undefined')
        if (err.code === 'FEATURE_DISABLED') console.error('feature off:', err.feature)
        if (err.originClientId) console.warn('error from other player:', err.originClientId)
    }
})
await cb.game.deactivateScript(appId, 'old-script')  // 비활성화 (코드 보존)
await cb.game.deleteScript(appId, 'really-old')      // 영구 삭제 (신규)

// createRoom 응답 검증 패턴 (NJB regression 가드)
const { state, scriptName, scriptVersion } = await room.createRoomDetailed({ scriptName: 'njb-main' })
if (scriptName !== 'njb-main') {
    throw new Error(`script attach mismatch: expected njb-main got ${scriptName}`)
}
console.log('attached', scriptName, 'v', scriptVersion)
```

## [3.13.1] - 2026-05-13

### Documentation — README batch/transaction 예제 타입 회귀 수정

README 의 `cb.database.batch()` / `cb.database.transaction()` 예제가 구 시그니처
(`table: 'string'`) 를 사용해 `tsc --noEmit` 에서 `TS2353` 발생하던 문제 수정.

- `table:` → `table_id:` (UUID, 실제 `BatchOperation` 타입과 일치)
- 3.12+ 의 `success:false` throw 동작 반영 — try/catch 패턴 명시

코드 변경은 없음. README 만 갱신.

## [3.13.0] - 2026-05-13

### Removed — `PlatformIssueDetail.triage_summary` / `triaged_at` 필드

ConnectBase 가 Platform Issue 의 AI 자동 분류 (triage) 기능을 통째 제거함에 따라
SDK 의 응답 타입에서도 다음 필드를 제거.

```typescript
// 이전 (3.12 이하)
interface PlatformIssueDetail {
  triage_summary?: string  // ← 제거
  triaged_at?: string      // ← 제거
  // ...
}

// 3.13+ — AI 요약 필드 없음. status / resolution_note / external_links 로 진행 확인.
```

`status='triaged'` enum 값은 운영자 수동 분류 의미로 유지된다 (코드 변경 불필요).

### Behavior change — `cb.support.reportPlatformBug` 의 category/severity 자동 보정 없음

이전: AI triage 가 발행 직후 비동기로 category / severity 를 보정.
3.13+: 운영자가 admin 콘솔에서 수동 조정. SDK 발행자가 지정한 값이 그대로 유지된다.

### Backend wiring (참고)

- `backend/cmd/core-server/app/worker/platform_issue_triage/` 워커 전체 제거
- `ent` 컬럼 6개 (`triage_summary`/`triage_keywords`/`triage_similar_issue_ids`/`body_embedding`/`triaged_at`/`triage_skip_reason`) 드롭
- `POST /v1/admin/platform-issues/:id/retriage` + `/bulk-retriage` endpoint 제거
- MCP `admin_retriage_platform_issue` 도구 제거

## [3.12.0] - 2026-05-13

### Fixed — `cb.database.batch()` / `cb.database.transaction()` silent success 회귀 (platform-issue 019e1c9c)

서버가 `success: false` + 개별 op `error` 로 부분 실패를 표현해도 SDK 가 그냥 Promise 를 resolve 해
호출자가 "성공" 으로 오해하던 문제 수정. 이제 첫 실패 op 의 error 메시지로 `throw`.

```typescript
try {
  await cb.database.batch([{
    type: 'update',
    table_id: '019d86bf-...',
    doc_id: '019de33a-...',
    operators: { like_count: { type: 'increment', value: 1 } },
  }])
} catch (e) {
  // 이제 RLS 거부, table_id 오타, 검증 실패 등이 명확히 throw 됨
  console.error(e.message)
}
```

### Added — `BatchWriteResult` / `TransactionResult` / `BatchOperationResult` / `TransactionWriteResult` 타입 export

batch/transaction 응답이 `{ results: Record<string, unknown>[] }` 에서 정형 타입으로 변경:

```typescript
import type { BatchWriteResult, BatchOperationResult } from 'connectbase-client'

const r: BatchWriteResult = await cb.database.batch(ops)
// r.success: boolean
// r.results[i]: { index, success, doc_id?, error? }
// r.total_count / success_count / failed_count
```

### Behavior change — batch/transaction 부분 실패가 더 이상 silent 통과되지 않음

이전: `success: false` 응답도 `await cb.database.batch(...)` 가 정상 resolve → 호출자가 매번
`response.success` 와 `response.results[i].success` 를 직접 확인해야 했음.

본 버전부터: `success: false` 면 즉시 `throw new Error(<첫 실패 op error>)`. 부분 실패를
silent 처리하던 코드가 있다면 try/catch 로 감싸야 한다.

### Backend wiring (참고)

본 fix 는 data-server 의 다음 변경과 짝을 이룬다 (별도 배포 필요):
- `transaction_service` 의 3 callsite (executeRead / RunTransaction commit / BatchWrite) 가
  `table_id` (UUID) 를 title 로 잘못 lookup → 항상 "ent: table not found" 회귀를 함께 수정
  (platform-issue 019e1c9c/eba6).
- batch/transaction 이 RLS 평가를 통째로 건너뛰던 보안 회귀 수정. operators 가 머지된 finalData
  를 newData 로 RLS 에 전달해 `.update` predicate 가 실제 변경 결과 기준으로 평가됨
  (platform-issue 019e1c9c/c155).

## [3.11.0] - 2026-05-10

### Fixed — `cb.game.createClient().createRoom()` 의 `scriptName` / 기타 wire 필드 매핑 (platform-issue 019e123a)

게임 룸 생성 시 SDK 의 camelCase config (`scriptName`, `tickRate`, `maxPlayers`, `roomId`,
`categoryId`) 가 게임 서버 (Go 핸들러 JSON 태그 = snake_case) 가 기대하는 와이어 형식으로
매핑되지 않아, **`scriptName` 미전달 → `RoomConfig.ScriptID` 비어 있음 → `onTick` /
`onPlayerJoin` 등 모든 사용자 Lua hook 이 silent skip** 되던 문제 수정. NJB 앱 보고로 확인.

### Added — `GameRoomConfig.scriptName`

```typescript
const room = cb.game.createClient({ clientId: 'p1' })
await room.connect()
await room.createRoom({
  tickRate: 20,
  maxPlayers: 100,
  scriptName: 'njb-main', // ← 콘솔/REST 로 업로드+활성화한 Lua 스크립트 이름
})
```

지정 시 룸의 `onRoomCreate` / `onPlayerJoin` / `onTick` / `onAction` / `onLeave` 가 해당
스크립트로 디스패치된다. 미지정 시 server tick + delta 만 흐르고 사용자 hook 은 호출되지 않음.

### Added — `toCreateRoomWire(config)` 헬퍼 export

테스트/디버깅 용으로 `import { toCreateRoomWire } from 'connectbase-client'`. SDK 가 내부적으로
`create_room` WebSocket 메시지 생성 시 사용하는 매핑 함수.

### Behavior change — `tickRate` / `maxPlayers` 등이 이제 실제로 적용됨

이전 버전은 camelCase 그대로 와이어로 보냈으나 게임 서버가 snake_case 만 인식해 모든 룸이
**서버 기본값 (tick_rate=64, max_players=100)** 으로 생성됐다. 본 버전부터 SDK 가 명시한 값이
실제로 반영된다. 이전 동작에 의존한 코드가 있다면 명시 값을 검토할 것.

### Backend wiring (참고)

본 fix 는 game-server 의 다음 변경과 짝을 이룬다 (별도 배포 필요):
- `CreateRoomRequest` 에 `script_name` (+ `script` alias) 필드 추가, `handleCreateRoom` 이
  `RoomConfig.ScriptID = appID:scriptName` 으로 매핑
- 신규 `scripts.Loader` — 게임 서버 부팅 시 활성 스크립트 일괄 로드 + `game.scripts.reload.>`
  NATS 구독으로 Activate/Rollback/Disable 이벤트를 ScriptEngine 에 반영. legacy `HotReloader`
  는 다른 subject/schema 라 미사용 코드였음 (이번에 발견된 누락)

## [3.10.0] - 2026-05-10

### Fixed — `/v1/auth/re-issue` 일시 실패에 강제 로그아웃 (platform-issue 019e11cf)

`HttpClient.refreshAccessToken()` 가 모든 실패에 즉시 토큰을 폐기해, 5xx / 네트워크 hiccup
한 번에 활동 중인 사용자가 강제 로그아웃되던 문제를 수정. OAuth 클라이언트 표준
(Auth0 SPA SDK / MSAL / Amplify) 에 맞춰 실패를 분류한다.

| 응답 | 분류 | 토큰 처리 | 호출되는 콜백 |
|---|---|---|---|
| `5xx` / 네트워크 / abort / 손상된 200 | transient | **보존** (다음 호출에서 backoff 후 재시도) | `onTransientRefreshFailure`, `onAuthError` |
| `401` / `403` / `400 invalid_grant` / `400 invalid_token` | permanent | 폐기 | `onTokenExpired`, `onAuthError` |
| 그 외 4xx (예: `400 invalid_request`) | client_bug | **보존** (재시도 무의미, 패치 배포로 회복) | `onAuthError` |

### Added — `onTransientRefreshFailure` 콜백

일시적 refresh 실패 시 호출되는 새 콜백. 토큰은 살아있으므로 강제 로그아웃 없이
"연결이 잠시 불안정합니다" 같은 비파괴 알림을 띄울 때 사용:

```typescript
const cb = new ConnectBase({
  publicKey: '...',
  onTransientRefreshFailure: () =>
    toast.warn('연결이 잠시 불안정합니다. 잠시 후 자동 복구됩니다.'),
  onTokenExpired: () => { window.location.href = '/login' },
})
```

### Behavior change (semver minor 로 분류한 이유)

`onTokenExpired` 의 호출 시점이 **refresh token 자체가 무효화된 경우로 한정**된다.
이전 버전에서는 transient 실패에도 호출됐기에, 그 콜백을 "임의 refresh 실패 알림" 으로
사용하던 앱은 동작 변화가 있다. transient 알림은 새 `onTransientRefreshFailure` 로 분리.
`onAuthError` 는 모든 실패에서 호출되는 점은 동일.

## [3.9.0] - 2026-05-10

### Added — Support API (end-user issue reporting)

앱 사용자가 운영자에게 버그·질문·요청을 발행하는 채널을 SDK 메서드로 노출.

```typescript
await cb.support.reportIssue({
  title: "결제 화면이 멈춰요",
  body: "결제 버튼 클릭 후 로딩이 끝나지 않습니다.",
  category: "bug",
  metadata: { pageUrl: window.location.href },
  // 익명 발행
  anonymousEmail: "user@example.com",
  recaptchaToken: await grecaptcha.execute(SITE_KEY, { action: 'report_issue' }),
})
```

- **AppMember JWT 자동 첨부**: 로그인 사용자는 `reporter_member_id` 자동 채워짐
- **익명 발행 지원**: AppMember 없어도 발행 가능 (운영자가 reCAPTCHA 활성화한 경우 토큰 권장)
- **응답 최소화**: `{ id, status: 'open', created_at }` 만 — 봇이 ID 구조 학습 회피
- **AI 자동 triage**: 백엔드(core-server)가 발행 직후 비동기로 요약·긴급도·카테고리·키워드·유사이슈 자동 분류
- **카테고리**: `bug` | `question` | `feature_request` | `incident` | `other`

서버 측 가이드: [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).

## [3.8.1] - 2026-05-07

### Fixed — `/v1/auth/re-issue` 가 콘솔/SDK cookie 공존 시 잘못된 토큰 발급

3.8.0 의 cookie 우선순위가 [member → user → Bearer] 로 고정되어, 같은 브라우저에
platform 콘솔용 `refresh_token` 과 SDK 용 `cb_member_refresh_token` 이 공존할 때
(예: 외부 앱 SDK 로그인 후 콘솔 로그인) 콘솔의 `/v1/auth/re-issue` 호출이
AppMember access token 을 받아 콘솔 측 endpoint(예: `/v1/cli-auth/approve/:id`,
`claims.UserID == nil`) 에서 401 발생하던 회귀를 수정했습니다.

- SDK 의 `refreshAccessToken()` 이 `/v1/auth/re-issue` 호출 시 `X-Public-Key` 헤더를
  첨부합니다. 백엔드는 이 헤더로 SDK 호출(member cookie 우선) vs 콘솔 호출
  (user cookie 우선) 을 식별합니다.
- 백엔드 `auth_controller.ReIssueAccessToken` 우선순위:
  1. `Authorization: Bearer` (가장 명시적)
  2. cookie 단독 시 X-Public-Key 헤더 유무로 분기
- Bearer 흐름 / 단일 cookie 흐름은 영향 없음. 회귀 시나리오만 정상화.

3.8.0 사용자는 3.8.1 로 업데이트하면 됩니다. 백엔드 측은 함께 배포 필요.

## [3.8.0] - 2026-05-07

### Added — HttpOnly cookie 기반 refresh token 흐름 (XSS 면역 default 세션)

3.7.x 의 `persistence` 콘솔 경고가 권고하던 "HttpOnly cookie + 기본값('none')" 흐름을
실제로 동작하도록 SDK + 백엔드를 함께 구현했습니다.

- `persistence: 'none'` (기본값) 으로도 **새로고침 후 자동 복구** 가능. refresh token 은
  서버 HttpOnly cookie 로만 보관되어 JS 가 접근할 수 없습니다 (XSS 시 탈취 불가).
- `localStorage` / `sessionStorage` 옵션은 여전히 사용 가능하지만 위험 경고가 유지됩니다.

**SDK 변경:**

- 모든 ConnectBase API fetch 호출에 `credentials: 'include'` 적용 — HttpOnly refresh cookie 가
  자동 첨부됩니다 (`api.connectbase.world` host-only cookie 기준).
- `/v1/auth/re-issue` 호출이 cookie 만으로 동작 — 메모리에 refresh token 이 없어도 cookie 가
  있으면 access token 회복.
- `ConnectBase` 옵션에 `autoRestoreSession?: boolean` 추가 (브라우저 기본 true). 인스턴스 생성
  시 자동으로 cookie 기반 세션 복구를 시도하며, 미로그인/cookie 만료 시 silent 실패.
- `cb.restoreSession(): Promise<boolean>` 메서드로 명시적 await 도 가능.
- `persistence` 콘솔 경고를 갱신: 위험은 그대로 표시하되 'none' + HttpOnly cookie 흐름이
  실제로 작동함을 안내.

**백엔드 변경 (core-server):**

- `pkg/util/cookie` 에 `CrossSite` / `HostOnly` 옵션 추가 (SameSite=None + Secure + host-only).
- 새 공용 헬퍼 `core-server/app/util/auth_cookie/` — platform 용 `refresh_token` 과 AppMember/OAuth
  용 `cb_member_refresh_token` 두 종류를 분리해 충돌 방지.
- `/v1/public/app-members/{signin,signup,signout}` + `CreateGuestMember` + OAuth callback/exchange
  엔드포인트가 refresh token 을 HttpOnly cookie 로 발급.
- `/v1/auth/re-issue` 가 입력 우선순위 `[member cookie → user cookie → Authorization Bearer]`
  로 처리하며, cookie 흐름은 sliding (재호출 시 cookie 만료 7일 연장).
- 응답 body 의 `refresh_token` 은 하위호환을 위해 그대로 유지 (구버전 SDK / Node.js / 게임 SDK
  호환). 신규 SDK 는 cookie 만 신뢰합니다.

**마이그레이션:**

기존 `persistence: 'sessionStorage'` 또는 `'localStorage'` 사용 중이었다면 옵션을 제거하기만
하면 됩니다 (default 가 안전 흐름):

```ts
// before (3.7.x)
new ConnectBase({ publicKey, persistence: 'sessionStorage' })

// after (3.8.0)
new ConnectBase({ publicKey })  // persistence: 'none' + autoRestoreSession: true (기본)
```

다른 origin 에서 호출하는 경우 (앱 도메인 ≠ `api.connectbase.world`) CORS 화이트리스트에 등록되어
있어야 합니다 — 콘솔의 커스텀 도메인 등록 흐름이 그대로 적용됩니다.

## [3.7.2] - 2026-05-01

### Fixed — `cb.endpoint.connectWebSocket()` 메시지 깨짐 + permessage-deflate 누설

3.7.0/3.7.1 의 endpoint WS pass-through 가 handshake 는 101 정상이지만 모든
메시지가 binary frame 으로 도착하고 payload 가 origin (ComfyUI/aiohttp) 의
raw WS frame bytes (RSV1=1 압축 frame 포함) 라 client 의 native WebSocket
이 디코드 불가하던 문제를 수정했습니다. 두 개의 별개 버그가 합쳐진 회귀:

1. **CLI 가 WS frame parse/encode 안 함** — 기존 `startWSStream` 이
   `socket.on('data')` 의 raw bytes 를 그대로 v2 binary frame payload 로
   forward 해서 client 에 frame header (0x81/0x82/0xc1) 가 섞인 상태로 도달.
2. **`Sec-WebSocket-Extensions` 가 upstream 까지 forward 됨** — 브라우저 native
   WebSocket 이 default 로 `permessage-deflate` 를 광고하고, sanitize 가 hop-by-hop
   만 strip 해서 upstream (aiohttp default) 이 accept → compressed frame 송신
   시작. CLI 는 deflate context 가 없어 디코드 불가.

**Fix (CLI):**

- `UpstreamWsFrameParser`: incoming WS frame 을 parse 해서 payload 만 추출.
  TEXT/BINARY/CONTINUATION/PING/CLOSE 처리, fragmented frame 은 누적, RSV1
  (compression) frame 은 protocol error 로 거부.
- `buildClientFrame` / `createUpstreamTextFrame`: outgoing payload 를 RFC 6455
  §5.3 client masking 적용한 masked WS frame 으로 encode 후 upstream 에 write.
- 로컬 upstream 요청에서 `Sec-WebSocket-Extensions` 헤더 strip — proxy chain 이
  deflate context 를 end-to-end 로 carry 하지 못하므로 compression 자체를 비활성.

**회귀 영향 범위:** 3.7.0 의 모든 `cb.endpoint.connectWebSocket()` 사용자.
ComfyUI / vLLM 등 default-on compression origin 영향. 사용자가 3.7.2 로 업그레이드
+ 터널 재시작 시 즉시 정상화. text/binary 구분은 client 측에서 항상 binary
(ArrayBuffer) 로 도달 — JSON 은 `TextDecoder.decode` 로 string 변환 필요.
Opcode propagation 은 별도 follow-up.

**회귀 가드:** `UpstreamWsFrameParser` 단위 테스트 (TEXT payload 추출 / fragmented
누적 / RSV1 reject / TCP coalescing / split-across-chunks / PING callback) +
`buildClientFrame` masking round-trip + `WSStreamForwarder` e2e (real local HTTP
server 로 `Sec-WebSocket-Extensions` 미도달 검증).

## [3.7.1] - 2026-05-01

### Fixed — `cb.endpoint.connectWebSocket()` 가 502 로 떨어지던 회귀

3.7.0 의 endpoint WS pass-through 가 client 단에서 항상 CF 502 page 로 떨어지던
문제를 수정했습니다. Root cause 는 CLI 가 로컬 upstream HTTP 요청에 WebSocket
upgrade 헤더 (`Connection: Upgrade` + `Upgrade: websocket`) 를 누락한 것:
upstream (ComfyUI 등) 이 일반 GET 으로 인식하고 400 Bad Request 반환 → core-server
ReverseProxy 가 비-101 body 를 relay 하려다 `net/http: abort Handler` panic →
CF 가 corrupted response 를 자체 502 page 로 substitute.

**Primary fix (CLI):**

- `cli.ts` `startWSStream` 과 `tunnel-v2.ts` `WSStreamForwarder` 가 upstream
  요청에 `Connection: Upgrade` + `Upgrade: websocket` 명시적으로 set. tunnel-server
  의 `sanitizeRequestHeaders` 가 RFC 7230 hop-by-hop 으로 strip 하므로 CLI →
  upstream 새 hop 에서 다시 추가 필요.

**Defensive fix (core-server, 자동 deploy):**

- `ForwardWebSocket` `ModifyResponse` 에서 비-101 upstream 응답 시 ErrorHandler
  경로로 라우팅 — ReverseProxy 가 비-101 body 를 relay 하려다 panic 하는 path
  차단. Upstream 이 정당한 사유로 비-101 (rate limit / auth 실패 등) 반환할 때도
  깨끗한 502 + 진단 메시지가 client 까지 도달.

**회귀 영향 범위:** 3.7.0 의 모든 `cb.endpoint.connectWebSocket()` 사용자.
사용자가 3.7.1 로 업그레이드 + 터널 재시작 시 정상 동작.

**회귀 가드:** `tunnel-v2.test.ts` 가 `WSStreamForwarder` 의 upgrade 헤더 송신을
real local HTTP server 로 e2e 검증. Go 측 `proxy_service_test.go
TestForwardWebSocket_Non101UpstreamReturnsCleanError` 가 panic 없는 깨끗한 502
반환을 락인.

## [3.7.0] - 2026-05-01

### Added — `cb.endpoint.connectWebSocket()` 네이티브 WebSocket 지원

Endpoint Proxy v2 (Phase 5) 가 출하되어, SDK 사용자가 `cb.endpoint.connectWebSocket()`
한 줄로 ComfyUI / vLLM / 일반 모델 서버의 WebSocket 엔드포인트를 직접 연결할 수
있습니다. CLI 도 v2 endpoint (`/v2/tunnel/connect`) 를 사용하도록 전환.

> **요구사항:** 백엔드 v2 endpoint 가 먼저 배포돼 있어야 동작합니다. 프로덕션
> 배포 완료 후 SDK 업그레이드를 권장합니다.

> **알려진 이슈:** 3.7.0 / 3.7.1 에서 WS frame leakage / 502 회귀가 발견되어
> **3.7.2 이상 사용 권장**. 자세한 내용은 3.7.1 / 3.7.2 항목 참고.

## [3.6.0] - 2026-05-01

### Added — `cb.realtime.stream()` 멀티모달 메시지 (Vision) 지원

`cb.realtime.stream()` 의 `content` 필드가 `string` 외에도 OpenAI Vision spec
호환 array 형식 (`{ type: 'text', text }` 또는 `{ type: 'image_url', image_url: { url } }`
파트 배열) 을 받을 수 있게 됐습니다. 이미지 첨부 채팅을 SDK 한 줄로 작성 가능.

**예시:**

```ts
await cb.realtime.stream(
    [{
        role: 'user',
        content: [
            { type: 'text', text: '이 이미지에 보이는 동물은?' },
            { type: 'image_url', image_url: { url: 'https://.../cat.jpg' } },
        ],
    }],
    { onToken, onDone, onError },
    { provider: 'openai_compatible' },
)
```

**Provider 별 처리:**

- **`openai` / `openai_compatible` (vLLM, LM Studio, Ollama 등)**: content array 를
  그대로 passthrough — vLLM 의 Qwen-VL / LLaVA / InternVL 등 모든 OpenAI Vision
  호환 비전 모델 즉시 동작.
- **`claude`**: `data:image/...;base64,...` URI 는 Claude 의 `source.type=base64`
  콘텐츠 블록으로 변환, `https://...` URL 은 `source.type=url` 로 위임 (Anthropic
  API 가 fetch 담당). 텍스트 파트는 `type=text` 블록으로 변환.
- **`gemini`**: Gemini API 가 외부 URL fetch 를 지원하지 않으므로 SDK 가 사전
  fetch (timeout 30s, 20MB 사이즈 캡, image/* MIME 검증) → base64 `inline_data`
  로 변환. data URI 는 즉시 분해.

**SDK 타입 변경:**

- `StreamMessage.content`: `string` → `string | StreamContentPart[]` (union, 하위 호환).
- 신규 export: `StreamContentPart`, `StreamTextPart`, `StreamImageURLPart`.

**서버 변경:**

- socket-server `protocol.StreamMessage.Content` 를 `json.RawMessage` 로 변경 후
  새 helper `protocol.ParseStreamContent()` 가 string/array 를 통일 디코드.
- pkg/ai 의 `Message` 에 `Parts []ContentPart` 추가 — provider 별 wire format 으로
  분기 직렬화. `Message.Content` (string) 는 기존과 동일하게 유지 → 모든 string-only
  호출자 (core-server, mcp-server 등) 회귀 무영향.

**회귀 가드:** 11 multimodal provider 직렬화 테스트 + 11 protocol/handler 검증
테스트 + 3 convertMessages plumbing 테스트 추가.

## [3.5.3] - 2026-05-01

### Fixed — `connectbase tunnel --label` 의 `tunnel_id` 추출 실패

3.5.2 의 `parseArgs()` 패치로 `--label` / `--description` 이 정상 파싱되어
`registerEndpointBinding()` 까지 호출되는 것은 확인됐으나, 함수 진입 직후
`tunnel_id` 추출 단계에서 즉시 skip 되어 자동 등록이 여전히 한 번도
수행되지 않던 문제를 수정했습니다.

원인: 추출 로직이 `https://<id>.tunnel.connectbase.world` (subdomain 기반)
형식을 가정했으나, 실제 발급 URL 은 `https://tunnel.connectbase.world/<id>`
(path 기반) 입니다. `host.replace(/\.tunnel\.connectbase\.world$/, '')` 가
정규식 매칭에 실패해 입력을 그대로 반환 → `tunnelId === host` 조건으로
"tunnel_id 추출 실패" 분기 진입 → 자동 등록 skip.

- URL 파싱을 제거하고, 서버가 `tunnel_ready` 메시지에 직접 실어 보내는
  `tunnel_id` 필드를 그대로 사용하도록 변경 (서버 권위값 기준 — 향후 URL
  형식 변경에 영향받지 않음).
- `registerEndpointBinding()` 시그니처: `tunnelUrl` → `tunnelId` 로 단순화 +
  `export` 노출 → 단위 테스트 가능.
- `test/cli-register-endpoint.test.ts` 회귀 가드 5 케이스 신규 추가:
  201 (정상 등록 — body.tunnel_id 검증), description 기본값, baseUrl trailing
  slash 정규화, appId URL 인코딩, 409/401/5xx/네트워크 오류 시 호출부
  흐름 유지 (throw 하지 않음).

회귀 영향 범위: 3.5.2 의 `--label` 자동 등록 사용자 (해당 기능을 처음으로
정상 동작시킴). 다른 옵션 / 기본 `tunnel` 동작에는 영향 없음.

## [3.5.2] - 2026-05-01

### Fixed — `connectbase tunnel --label` / `--description` 옵션이 파싱되지 않던 문제

3.5.1 의 인증 헤더 / `await` 수정에도 불구하고 `--label` / `--description` 으로
endpoint binding 자동 등록이 한 번도 트리거되지 않던 문제를 수정했습니다.
원인은 `parseArgs()` 의 옵션 파싱 분기 자체가 두 옵션을 매칭하지 않아
`parsed.options.label === undefined` 로 떨어지고, 결과적으로
`if (tunnelOpts?.label && tunnelUrl)` 가드가 항상 false 가 되어
`registerEndpointBinding()` 이 호출되지 않았던 것입니다.

- `parseArgs()` 의 while 루프에 `--label` / `--description` 분기 추가
  (기존 `--storage` / `--max-body` 와 동일 패턴).
- `parseArgs()` 를 `export` 로 노출 + `test/cli-parse-args.test.ts` 회귀 테스트
  추가 — `--label` 단독 / `--label` + `--description` 조합 / 미지정 / `--description`
  단독 4 가지 케이스를 검증해 동일 회귀 차단.

회귀 영향 범위: 3.5.1 에서 `--label` / `--description` 을 사용했던 모든 호출
(즉 endpoint binding 자동 등록 기능 전체). 다른 옵션·기본 `tunnel` 동작에는 영향 없음.

## [3.5.1] - 2026-05-01

### Fixed — `connectbase tunnel --label` endpoint 자동 등록 401

`--label` 옵션 사용 시 호출되는 `POST /v1/apps/:appID/endpoints/cli` 가 모든 요청에서
401 ("유효하지 않은 토큰입니다") 으로 떨어져 endpoint binding 이 한 번도 등록되지
않던 문제를 수정했습니다. 결과적으로 `cb.endpoint.call("label", ...)` 가 항상
`endpoint not found` (404) 로 실패했습니다.

- CLI 가 `Authorization: Bearer cb_sk_*` 로 호출하던 것을 서버 dual-auth 미들웨어가
  요구하는 **`X-Public-Key: cb_sk_*`** 헤더로 변경.
- `void registerEndpointBinding(...)` 의 fire-and-forget 호출을 `await` 로 변경 —
  성공/실패 메시지가 "Ctrl+C 로 종료" 안내 앞에 출력됨.
- 등록 실패 / 네트워크 오류는 stdout 이 아닌 **stderr** (빨간색 ✗) 로 출력 —
  파이프/리다이렉트 환경에서도 사용자가 실패를 인지할 수 있도록.

회귀 영향 범위: `--label` 사용자만 해당 (기본 `connectbase tunnel <port>` 는 영향 없음).

## [3.5.0] - 2026-04-30

### Added — Knowledge Base 사용자별 격리

다중 사용자 SaaS RAG 챗봇 시나리오에서 한 KB 안의 문서를 사용자 단위로 격리할 수
있도록 검색 요청에 `where` 필드 + magic 토큰을 추가했습니다. **모든 격리 enforcement
는 서버측에서 수행** 되므로 클라이언트 변조 불가.

- **`KnowledgeSearchRequest.where?: Record<string, unknown>`** — metadata 기반 필터.
  키 형식 `metadata.<path>` 또는 raw key. 검색 시 청크 metadata JSON 의 path 매칭.
- **`AUTH_MEMBER_ID_TOKEN`** (`'$auth.member_id'`) — magic 토큰 export. `where` 값에
  사용하면 서버가 인증된 AppMember ID 로 자동 치환. AppMember JWT 가 함께 오지 않은
  호출에서 사용 시 401.

### Behavior — Authorization 헤더로 격리 활성화

`Authorization: Bearer <appmember-jwt>` 헤더가 함께 오면 서버가 자동으로:

1. 검색 결과를 본인 `metadata.user_id` 문서로 한정
2. `addDocument` 시 `metadata.user_id` 자동 태깅 (클라이언트가 다른 값을 넣어도 강제 덮어씀)
3. `listDocuments` 가 본인 자료만 반환
4. `deleteDocument` 가 본인 자료만 허용 (cross-user → 403)
5. AI chat 의 RAG 검색 (`cb.ai.chatStream({ knowledgeBaseId })`) 도 동일 격리

헤더가 없으면 기존 동작 그대로 — 앱 단위 공유. **하위 호환 보장**.

```ts
import ConnectBase, { AUTH_MEMBER_ID_TOKEN } from 'connectbase-client'

// 로그인된 사용자 컨텍스트
const cb = new ConnectBase({
  publicKey: 'cb_pk_...',
  authToken: appMemberJwt,  // AppMember JWT 동봉 시 격리 활성화
})

// 본인 자료만 검색
await cb.knowledge.search(kbId, { query: '내 메모' })

// 본인 자료 + 추가 필터
await cb.knowledge.search(kbId, {
  query: '내 메모',
  where: { 'metadata.tag': 'work' },
})
```

자세한 사용법: [docs/knowledge-base/USER_ISOLATION.md](https://github.com/connectbase-world/connectbase/blob/release/docs/knowledge-base/USER_ISOLATION.md)

## [3.4.0] - 2026-04-30

### Added — `cb.analytics.reset()` 사용자 전환 오염 방지 helper

같은 브라우저에서 다른 사용자로 로그인할 때 방문자 (`visitor_uid`) 데이터가 이전
사용자의 식별자에 묶인 채로 남아 데이터가 오염되던 문제를 SDK 측에서 끊어 낼 수
있는 helper 를 추가했습니다.

- **`cb.analytics.reset()`** — 로그아웃 시 호출. `visitor_uid` 를 새로 발급하고
  in-flight 큐를 비웁니다.
- **자동 reset (1) — `identify(memberId)` 가 다른 멤버 감지** — 직전 세션과 다른
  `memberId` 가 들어오면 SDK 가 자동으로 `reset()` → identify 순으로 처리.
- **자동 reset (2) — `linkMemberSilent` conflict 안전망** — 백엔드가 `409
  VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 주면 SDK 가 자동으로 reset 후 1회 재시도.
  명시적 `reset()` 호출을 누락한 경우의 fallback.

### Changed — BatchEvent 멱등성

- 모든 `BatchEvent` 에 `event_id` (UUID) 자동 부여. 백엔드가 `(visitor_id,
  event_id)` UNIQUE 인덱스 + `OnConflict DoNothing` 으로 at-least-once 재전송 시
  중복 INSERT 차단. SDK 사용자 입장에서 추가 작업 없음.

### Fixed — 첫 방문 봇 오판 방지

- `flushSync` (`navigator.sendBeacon`) 호출에 `user_agent` 를 첨부 — 첫 방문 직후
  `pagehide` 로 flush 될 때 백엔드가 UA 누락으로 봇으로 분류해 카운트가 누락되던
  문제 해결.

**서버 측 동시 변경:** `LinkMember` idempotent 처리, `WebPageView.event_id` 컬럼 +
UNIQUE 인덱스, `link-member` 옵셔널 토큰 검증 (Authorization Bearer 가 있으면
AppMember 토큰의 `member_id` 와 body 일치 검증, 없으면 BC 보존 + 경고 로그).

## [3.3.1] - 2026-04-30

### Fixed — Docs

3.3.0 publish 직후 SDK 감사에서 `README.md` 의 `### Game Server` 섹션에 신규 API
`cb.game.config` 가 누락된 것을 발견. npm 페이지의 사용자 1차 문서 정합성을 위해
patch release 로 보강.

- README 의 `## API Reference > ### Game Server` 아래에 `#### cb.game.config — Feature
  Opt-in (v3.1+, SDK 3.3.0+)` 섹션 추가 — get/set/enable/disable 4개 메서드 시그니처,
  partial PATCH 의미, HTTP 응답 코드 표 (403 / 429 / 402), OPT_IN.md cross-link.
- 코드 동작 변화 없음. README/CHANGELOG 만 수정.

## [3.3.0] - 2026-04-30

### Added — `cb.game.config` (게임 기능 opt-in 토글)

게임 서버 v3.1 의 7개 기능 (matchqueue / leaderboard / entity / scripts / voice / replay /
spectator) 이 모두 앱 단위 명시적 opt-in 정책으로 전환됨에 따라, 콘솔 / 외부 도구가
토글 상태를 조회/변경할 수 있는 SDK 헬퍼 신규 추가.

- **`cb.game.config.get(appId?)`** — 현재 7개 토글 상태 + `legacy_defaults` 플래그.
  row 가 없는 기존 앱은 모든 기능 ON 상태로 응답 (서비스 단절 방지). `legacy_defaults=true`
  를 받으면 backfill 권장.
- **`cb.game.config.set(appId, patch)`** / **`cb.game.config.set(patch)`** — partial
  update. 보낸 필드만 갱신, 나머지는 보존. PATCH 직후 game-server 캐시는 NATS
  publish 로 즉시 무효화 (또는 30s TTL).
- **`cb.game.config.enable(appId, feature)`** / **`disable(appId, feature)`** —
  단일 토글 편의 wrapper.

```ts
const cfg = await cb.game.config.get(appId)
if (!cfg.matchqueue_enabled) {
  await cb.game.config.set(appId, { matchqueue_enabled: true, leaderboard_enabled: true })
}
```

신규 앱은 모든 기능 OFF 가 기본값. 사용 안 하는 앱은 noisy-neighbor / 의도치 않은
quota 소비에서 격리됨.

자세한 정책: [docs/game-server/OPT_IN.md](https://github.com/connectbase-world/connectbase/blob/release/docs/game-server/OPT_IN.md)

### Behavior — 비활성 기능 호출 시 응답 변화

본 버전부터, 비활성 feature 의 HTTP 호출은 **HTTP 403 + `{error: "feature_disabled", feature, hint}`**
를 반환. 기존 SDK 호출 코드는 catch 에서 status 403 + error 코드를 분기해 사용자에게
"콘솔에서 켜주세요" 메시지를 표시하는 게 권장됩니다.

## [3.2.1] - 2026-04-29

### Fixed — Docs

`cb.endpoint.url(label, path)` 의 README/JSDoc 예제와 CHANGELOG `[3.2.0]` 설명을
ConnectBase 프록시 인증 모델에 맞춰 정정. 코드 동작 변화 없음 (JSDoc / README /
CHANGELOG 만 수정).

- 3.2.0 의 `<img src={cb.endpoint.url(...)}>` / `new WebSocket(cb.endpoint.url(...))`
  예제는 실제로는 401 — `/v1/proxy/:label/*` 가 `X-Public-Key` 헤더를 강제하고
  쿼리 파라미터 폴백이 없어, 커스텀 헤더를 못 보내는 브라우저 네이티브 API
  (`<img>`, native `WebSocket`, `<script src>`, `EventSource`) 는 인증 자체가
  불가능. 잘못된 시연 코드를 제거.
- 정정된 사용 사례:
  - **이미지 렌더링**: `cb.endpoint.call(...)` 로 받아
    `URL.createObjectURL(await res.blob())` 패턴 사용. 영구 URL 이 필요하면
    `cb.storage.uploadByPath` 로 업로드.
  - **URL 전달**: Service Worker / 백엔드 워커 / 커스텀 fetch wrapper 처럼 호출자가
    `X-Public-Key` 헤더를 직접 부착할 수 있는 환경.
  - **로깅·디버깅**: 라벨 → 최종 URL 매핑 확인.
- `EndpointAPI` 클래스 / `cb.endpoint.{call,url,pollUntil}` 동작은 3.2.0 과 동일.

## [3.2.0] - 2026-04-29

### Added — Endpoint API 헬퍼 (`pollUntil` / `url`)

ComfyUI × 웹스토리지 같은 e2e 통합 패턴 (작업 제출 → 폴링 → 결과 저장) 을
단일 진입점으로 묶기 위한 헬퍼 2개. `cb.endpoint.call()` 만으로도 가능했지만
사용자가 매번 직접 작성하던 보일러플레이트를 SDK 가 흡수.

- **`cb.endpoint.pollUntil<T>(label, init, predicate, opts)`** — long-poll 한 줄
  처리. ComfyUI `/history/{id}`, A1111 `/sdapi/v1/progress`, 자체 큐 API 처럼
  "작업 제출 → 폴링" 패턴 전용. `predicate` 가 값을 반환할 때까지 반복 호출,
  HTTP 5xx/네트워크 오류는 재시도, 4xx 는 즉시 reject, `AbortSignal`/`timeoutMs`
  지원. `parse: "json" | "text" | "none"` 으로 본문 파싱 방식 선택.
- **`cb.endpoint.url(label, path)`** — 라벨 + path 의 최종 호출 URL
  (`${baseUrl}/v1/proxy/${label}${path}`) 만 조립해서 반환. URL 을 Service
  Worker / 백엔드 워커로 넘기거나 로깅·디버깅 용도. ⚠️ `<img src>` / 네이티브
  `WebSocket` / `<script src>` / `EventSource` 처럼 커스텀 헤더를 못 보내는
  브라우저 API 에 직접 넘기면 401 — ConnectBase 프록시는 항상 `X-Public-Key`
  헤더를 요구하고 쿼리 파라미터 폴백이 없음. 그 경우엔 `call()` 로 받아
  `URL.createObjectURL(await res.blob())` 패턴 사용.
- 신규 export: `PollUntilOptions` 타입.

### Docs

- `examples/ai-image-generator/` — UMD CDN 한 줄 + 빌드 도구 0 으로 동작하는
  ComfyUI × 웹스토리지 스타터. SDK 로딩 실패 감지, pre-flight 키 검증, 모든
  KSampler seed 랜덤화, cache-bust 워크플로우 fetch, AbortController 일괄 취소,
  step indicator, localStorage 갤러리, ⌘/Ctrl+Enter 단축키 등 11가지
  베스트프랙티스 채택.
- `docs/integration/comfyui-web-storage.md` — e2e 통합 가이드 + "왜 이 구조가
  정답인가" / "안티패턴 7가지".

## [3.1.0] - 2026-04-29

### Added — Endpoint API (로컬 모델 터널 dumb pipe)

사용자 PC GPU 모델 (ComfyUI / A1111 / Hunyuan3D / vLLM / 자체 FastAPI 등) 을
`cb_pk_*` 한 키로 호출하는 새 모듈. ConnectBase 는 모델·API·워크플로우를
알지 않고, 라벨 → tunnel 매핑만 들고 페이로드/응답을 그대로 통과시킵니다.

- **`cb.endpoint.call(label, init)`** — fetch() 시그니처 호환 (path, method,
  headers, body, signal). URL 은 `${baseUrl}/v1/proxy/${label}${path}` 로
  자동 조립, `X-Public-Key` 헤더 자동 주입. SSE / chunked 스트리밍은
  `res.body.getReader()` 로 그대로 읽기.
- 신규 export: `EndpointAPI`, `EndpointCallInit`.

### Added — CLI

- **`connectbase tunnel <port> --label <name>`** — tunnel 발급 후 endpoint
  binding 을 자동 등록. SDK 사용자가 즉시 `cb.endpoint.call("<label>", { ... })`
  로 호출 가능. 인증은 User Secret Key (`cb_sk_*`) — dual-auth 라우트
  `POST /v1/apps/:appID/endpoints/cli`.
- **`--description <text>`** — endpoint binding 의 설명 (`--label` 동반 시만).
- 이미 등록된 라벨이면 경고 후 진행 (다른 tunnel_id 로 갱신은 콘솔에서 PATCH).

### Fixed

- JSDoc 안 중첩 블록주석으로 빌드가 깨지던 회귀 수정 (`*/` 가 주석을 조기
  종료시키던 케이스).

## [3.0.1] - 2026-04-28

### Fixed

- `npx connectbase docs` 가 인증 없이 곧장 문서를 받도록 수정. 백엔드 `/v1/storages/webs/claude-md`
  는 public 라우트인데 CLI 가 불필요하게 브라우저 인증 → 앱 선택 → Public Key 발급을 강제하던
  흐름을 제거. 캐시된 publicKey 가 있으면 문서에 박아주고, 없으면 백엔드가 placeholder
  (`YOUR_PUBLIC_KEY_HERE`) 로 대체해 그대로 다운로드.

## [3.0.0] - 2026-04-28 — BREAKING

게임 서버 mechanism-only 재설계. ConnectBase 가 박아두던 게임 룰 (파티/로비/랭킹/매치메이킹/
killcam/highlight) 을 모두 제거하고, primitive (`cb.game.matchqueue`, `cb.game.leaderboard`,
`cb.game.scripts`) + 사용자 Lua 로 대체. 1:1 마이그레이션은 [MIGRATION_v3.md](../../../docs/sdk/MIGRATION_v3.md).

### Removed (BREAKING)

- **Lobby**: `listLobbies`, `createLobby`, `getLobby`, `joinLobby`, `leaveLobby`, `toggleReady`,
  `startGame`, `kickPlayer`, `updateLobby`, `sendLobbyChat`, `invitePlayer`, `acceptInvite`,
  `declineInvite`, `getPlayerInvites` 메서드 + 관련 타입 (`LobbyInfo`, `CreateLobbyRequest`,
  `UpdateLobbyRequest`, `LobbyInvite`)
- **Party**: `createParty`, `joinParty`, `acceptPartyInvite`, `declinePartyInvite`,
  `leaveParty`, `kickFromParty`, `inviteToParty`, `getParty`, `getMyParties`, `setReady`,
  `setPartyMetadata`, `getPartyInvites`, `sendPartyChat` + `PartyInfo`, `PartyInvite`
- **Matchmaking**: `joinQueue`, `leaveQueue`, `getMatchStatus` + `JoinQueueRequest`,
  `MatchmakingTicket`
- **Ranking**: `getLeaderboard`, `getPlayerStats`, `getPlayerRank` + `LeaderboardEntry` (구
  player_id/rating/tier/wins/losses 시그니처. 신규 `LeaderboardScoreEntry` 가 대체).
- **Killcam/Highlight**: `cb.game.replay.killcam(...)`, `cb.game.replay.highlights(...)`
  관련 endpoint 가 백엔드에서 제거되어 SDK 호출 시 404. 사용자가 `cb.game.replay.download`
  로 raw frame 받아 클라/Lua 에서 직접 처리.

### Added — v3 primitive

- **Matchqueue** (`cb.game.matchqueue.*` → `enqueueMatch / listMatchqueue / cancelMatch`):
  rating/region 등 attributes 는 free-form. 매칭 알고리즘은 사용자 Lua 가 list → notify.
- **Leaderboard** (`submitScore / getTopScores / getMemberRank / getRankAround / resetLeaderboard / removeFromLeaderboard`):
  ELO/티어/시즌 박지 않음. 시즌은 key suffix (`ranks:2026q2`) 로 분리, 점수 공식은 Lua.
- **Scripts** (`uploadScript / listScripts / getScript / listScriptVersions /
  activateScript / rollbackScript / disableScript`): Lua 스크립트 영속 메타 + 버전 + hot reload.
- 신규 타입: `MatchqueueTicket`, `MatchqueueListResponse`, `LeaderboardScoreEntry`,
  `LeaderboardListResponse`, `ScriptMeta`, `ScriptVersion`, `ScriptListResponse`,
  `ScriptVersionListResponse`, `ScriptDetailResponse`.

### Backend 영향

- game-server v3 부터 `/v1/game/:appID/matchqueue/:key/*`, `/leaderboards/:key/*`,
  `/scripts/*` 라우트가 유일한 게임 메커니즘 endpoint. lobby/party/ranking/matchmaking
  엔드포인트는 모두 404.
- 자세한 변경: [`docs/sdk/MIGRATION_v3.md`](../../../docs/sdk/MIGRATION_v3.md),
  [`docs/game-server/RECIPES.md`](../../../docs/game-server/RECIPES.md).

### Migration 요약

```ts
// Before (v2.x)
await cb.game.matchmaking.join({ mode: "ranked", rating: 1500 })
await cb.game.ranking.submit({ leaderboard: "elo", score: 2150 })

// After (v3.0)
await cb.game.enqueueMatch(appId, "ranked", userId, { rating: 1500 })
await cb.game.submitScore(appId, "elo", userId, 32, "incr")
```

## [2.0.0] - 2026-04-27 — BREAKING

Realtime presence/typing 단일화의 최종 단계. 1.13 deprecation 단계를 건너뛰고 즉시
deprecated 코드를 제거. presence/typing 의 단일 SoT 는 `cb.realtime.*`.

### Removed (BREAKING)

- **`cb.database.setPresence(status, device, metadata)`** — 코드 자체 제거. 호출 시 `TypeError`.
- **`cb.database.subscribePresence(userIds, callback)`** — 코드 자체 제거.
- **`DatabasePresenceState`** type — `PresenceInfo` (from `'./realtime'`) 사용.
- 1.13 에서 추가됐던 deprecation `console.warn` 헬퍼도 함께 제거 (필요 없음).

### Migration

```ts
// Before
cb.database.setPresence('online', 'web', { nickname: '홍길동' })
cb.database.subscribePresence(['user1', 'user2'], (states) => {
  console.log(states['user1']?.last_seen)
})

// After
await cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })
const unsub = await cb.realtime.subscribePresence('user1', (info) => {
  console.log(info.lastSeen)
})
```

자세한 가이드: [MIGRATION-v2.md](./MIGRATION-v2.md). 필드 차이는 `user_id`→`userId`,
`last_seen` (ISO) → `lastSeen` (epoch ms), `'busy'` 상태 추가.

### Server-side

데이터 서버 측 presence/typing 코드도 함께 제거되었습니다. `presence_set` / `presence_subscribe`
/ `typing_*` 메시지를 데이터 서버 WebSocket 으로 보내면 `USE_SOCKET_SERVER` 에러가 반환됩니다.

## [1.13.0] - 2026-04-27

Realtime presence/typing 단일화 — `cb.database.setPresence` / `cb.database.subscribePresence`
는 v2.0.0 에서 제거됩니다. 신규 코드는 `cb.realtime.*` 를 사용하세요. 자세한 마이그레이션은
[MIGRATION-v2.md](./MIGRATION-v2.md) 참고.

### Deprecated

- **`cb.database.setPresence(status, device, metadata)`** → `cb.realtime.setPresence(status, { device, metadata })`
- **`cb.database.subscribePresence(userIds, onPresence)`** → `cb.realtime.subscribePresence(userId, handler)` 또는
  `cb.realtime.onPresenceChange(handler)` (다중 사용자 일괄 수신).
- **`DatabasePresenceState`** type → `PresenceInfo` (from `'./realtime'`).
  - 필드 네이밍: `user_id` → `userId`, `last_seen` (ISO) → `lastSeen` (epoch ms 숫자).
  - 상태값: `'busy'` 추가 (PresenceInfo 만 지원).

호출 시 개발 환경(`process.env.NODE_ENV !== 'production'`)에서 `console.warn` 이 1회 출력됩니다.
런타임 동작은 1.12.x 와 동일합니다 (이번 버전은 deprecate 만, 코드 제거는 v2.0.0).

### Changed

- **WebSocket URL 기본값** `cb.database.connectRealtime` 의 기본 endpoint 가
  `/v1/realtime/ws` → `/v1/database/realtime/ws` 로 변경. 기존 경로는 30일간 alias 로
  유지되며 응답에 `Sunset` (RFC 8594) + `X-Deprecated-Endpoint` 헤더가 부착됩니다.
  명시적으로 `dataServerUrl` 을 지정한 경우 자동 변환은 하지 않습니다 (Hyrum's Law 준수).

### Migration

```ts
// Before (1.12.x)
cb.database.setPresence('online', 'web', { nickname: '홍길동' })
cb.database.subscribePresence(['user1', 'user2'], (states) => {
  console.log(states['user1']?.last_seen) // ISO string
})

// After (1.13+, 권장 — v2 호환)
cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })
const unsubscribe = await cb.realtime.subscribePresence('user1', (info) => {
  console.log(info.lastSeen) // epoch ms number
})
```

## [1.12.0] - 2026-04-26

Analytics 조회 정렬 옵션 정합성 수정 — `VisitorListOptions.sort_by` 의 TypeScript
union 을 백엔드가 실제로 인식하는 값으로 정정합니다. 1.10/1.11 의 SDK 타입은
`'last_visit' | 'total_visits' | 'total_page_views'` 였으나 백엔드 정렬 분기
(`web_visitor_repository.GetStorageVisitors`,
`web_visitor_service.GetVisitorGroupsByMember`) 는 `last_visit` 외 값을
인식하지 못해 항상 default(`last_visit`) 분기로 떨어졌습니다 — silent 잘못된 정렬.

### Changed — `VisitorListOptions.sort_by` 타입

- 변경 전 (1.10/1.11): `'last_visit' | 'total_visits' | 'total_page_views'`
- 변경 후 (1.12+): `'last_visit' | 'visits' | 'page_views' | 'first_visit'`
- 영향 메서드: `cb.analytics.getVisitors()`, `cb.analytics.getVisitorGroups()`.
- 런타임은 SDK 가 sort_by 문자열을 그대로 백엔드에 전달하므로, 새 값을 쓰면
  비로소 의도한 정렬이 동작합니다. 1.10/1.11 코드에서 `total_visits` /
  `total_page_views` 를 넘긴 호출은 실제로는 default `last_visit` 정렬 결과를
  받고 있었던 점에 주의.

### Migration

```ts
// Before (1.10/1.11): 컴파일은 통과하지만 런타임은 last_visit 정렬
cb.analytics.getVisitors('019d8...', { sort_by: 'total_visits' })

// After (1.12+): 의도한대로 visits 기준 정렬
cb.analytics.getVisitors('019d8...', { sort_by: 'visits' })
cb.analytics.getVisitors('019d8...', { sort_by: 'page_views' })   // 페이지뷰 누적 기준
cb.analytics.getVisitors('019d8...', { sort_by: 'first_visit' })  // 최초 방문일 기준
```

기존 코드가 `'last_visit'` 만 사용했다면 변경 불필요 (대부분의 호출).
타입 변경은 breaking 이지만 런타임 호환은 유지됩니다 — 이전 union 의 두 값은
원래 동작하지 않았기 때문.

## [1.11.0] - 2026-04-26

Analytics 사용자 통합 강화 — 멤버별 합산 방문자 조회, 단건 멤버 조회, 즉시 backfill,
Visitor merge admin API. sisun 팀의 1.10 이후 후속 피드백이 시작점이며, 어드민에서
"회원 단위 합산 방문자" 위젯을 위한 페이지네이션 풀 다운 우회 코드를 SDK 한 줄 호출로
대체할 수 있도록 했습니다. 모든 추가 메서드는 `cb.analytics` 네임스페이스에 들어가며
JWT/cb_sk_ dual-auth 로 호출할 수 있습니다.

### Added — `cb.analytics.getVisitorGroups(storageWebId?, options?)`

- 같은 `app_member_id` 의 visitor row 들을 서버 단에서 합산해 단일 row 로 반환.
  익명 visitor 는 단일 row 로 그대로 노출되어 페이지네이션 의미가 일관됨.
- 응답 필드: `app_member_id`, `visitor_uids[]`, `visitor_count`, `total_visits`,
  `total_page_views`, `first_visit_at`(MIN), `last_visit_at`(MAX), `country`, `is_bot`.
- `visitor_count` 는 **"디바이스 수" 가 아닌 "추적 브라우저 인스턴스 수"** 입니다 (시크릿/일반
  모드는 별도 카운트).
- 백엔드 `GET /v1/storages/web/:id/visitor-groups` 신설 (dual-auth).

### Added — `cb.analytics.getVisitorByMember(storageWebId?, memberId)`

- 어드민 회원 상세 페이지처럼 한 명만 필요할 때. 페이지네이션 풀 다운 없이 한 번 호출.
- 응답은 위 group item 과 동일 형태 (단건).
- 백엔드 `GET /v1/storages/web/:id/members/:member_id/visitor` 신설.

### Added — `cb.analytics.mergeVisitors(storageWebId?, request)`

- 두 visitor 가 동일인임을 외부에서 알게 됐을 때 admin 작업으로 통합. source 의 자식
  레코드 (page_views, daily, custom_events, experiment_assignments, heatmap_events,
  session_recordings) 를 target 으로 옮긴 뒤 source 삭제. 단일 트랜잭션, 부분 실패 시
  전체 롤백.
- 입력: `source_visitor_uid` 필수 + (`target_visitor_uid` 또는 `target_member_id`) 중
  하나 필수. `target_member_id` 만 주면 그 멤버의 가장 최근 활동 visitor 를 target 으로 잡음.
- 응답: `target_visitor_id`, `moved_records`(이전된 자식 레코드 수).
- 제약: `first_visit_at` 은 ent Immutable 필드라 target 값을 바꾸지 않습니다 (daily 가
  실제 이력 보존). source/target 은 같은 storage_web 에 속해야 합니다.
- 백엔드 `POST /v1/storages/web/:id/visitors/merge` 신설.

### Changed — `cb.analytics.identify(memberId)` 동작 보강

- 1.10 까지: `identify()` 호출 후 다음 batch 가 백엔드에 닿을 때 게스트 visitor 가 회원으로
  자동 연결됨 (백엔드 BatchRecordVisit 의 자동 LinkMember 로직).
- 1.11+: `identify()` 가 즉시 `POST /visitors/link-member` 를 한 번 호출하여 backfill 을
  당겨옵니다. 첫 페이지뷰 전 호출 시 404 발생 가능 — silent fail (다음 batch 가 자가 복구).
- `setMemberId(memberId)` 는 즉시 backfill 을 호출하지 않습니다 (이전과 동일 동작).

### Migration

기존 호출은 그대로 동작합니다. `identify()` 의 즉시 backfill 동작이 추가 HTTP 요청 1번을
유발하므로, 같은 효과를 다음 batch 까지 미루고 싶다면 `setMemberId()` 를 사용하세요.

## [1.10.0] - 2026-04-26

OAuth 콜백 응답 email 노출 + Analytics 조회 메서드 신규 + Functions 자동화용 Push 발송 메서드 신규.
sisun 팀(`019d85c7-...`) 의 운영 피드백 3건이 시작점이며, 백엔드 라우트는 콘솔 JWT 와 User Secret Key(`cb_sk_`)
둘 다 받는 dual-auth 로 확장되었습니다. 모든 변경은 추가 성격이며 기존 호출 시그니처는 유지됩니다.

### Added — OAuth 콜백 응답에 `email`

- `OAuthCallbackResponse.email?: string` — `signInWithPopup` / `getCallbackResult` /
  `exchangeCodeFromCallback` 모두 첫 응답에 이메일을 포함하도록 확장.
- 백엔드는 2026-04-19 부터 OAuth 신규 가입/재로그인 시 `AppMember.email` 을 자동 저장해왔으나,
  콜백 응답 DTO 에는 노출되지 않아 SDK 사용자가 추가로 `getMember()` 를 호출해야 했음. 이번
  릴리즈로 한 번의 로그인 응답만으로 이메일을 확보할 수 있다.
- Apple private relay / 이메일 권한 미동의 시 `email` 은 비어 있을 수 있다 (선택 필드).

### Added — Analytics 조회 메서드 (Functions / cb_sk_ 전용)

- `cb.analytics.getPopularPages(storageWebId?, options?)` — 인기 페이지.
- `cb.analytics.getNavigationFlow(storageWebId?, options?)` — 페이지 전환 플로우 (Sankey).
- `cb.analytics.getVisitors(storageWebId?, options?)` — 방문자 목록.
- 백엔드 라우트 `/v1/storages/web/:id/{popular-pages,navigation/flow,visitors}` 가 dual-auth 로 변경되어
  콘솔 JWT 외에 User Secret Key(`cb_sk_`) 호출도 허용. 어드민 앱이 Function 에서 호출해 자체
  대시보드를 구성할 수 있다.
- 브라우저 SDK (Public Key `cb_pk_`) 인스턴스에서는 호출 시 명확한 에러를 던져 권한 누설을 차단.

### Added — Push 발송 메서드 `cb.push.sendToMembers`

- `cb.push.sendToMembers(appId, memberIds, payload)` — Functions / 서버 자동화 환경에서
  회원 ID 목록으로 푸시 발송. payload 는 title/body/imageUrl/data/platforms/ttlSeconds/priority/
  clickAction/scheduledAt 지원.
- 백엔드 `/v1/apps/:appID/push/send` 가 cb_sk_ 인증을 받도록 dual-auth 로 변경. 1.9 까지 안내되던
  `CONSOLE_ACCESS_TOKEN` (만료 있는 콘솔 JWT) 우회 패턴이 더 이상 필요 없음.
- Public Key 인스턴스 호출 시 명확한 에러로 차단.

### Changed — 문서

- `embedded/sections/15-sdk-push.md` — Functions 발송 예제를 cb_sk_ 권장 패턴으로 갱신, legacy
  `CONSOLE_ACCESS_TOKEN` 마이그레이션 안내 추가.
- `embedded/sections/33-sdk-analytics.md`, `38-sdk-analytics-advanced.md` — REST 표에
  visitors/popular-pages/navigation/flow 라우트의 dual-auth 를 명시.

### Migration

기존 호출은 그대로 동작합니다.

- OAuth: `signInWithPopup()` 결과의 `email` 이 신규 추가 — 사용 안 해도 무방.
- Analytics 조회: 1.9 까지 SDK 에 조회 메서드가 없었으므로 신규 도입.
- Push 발송: Functions 코드의 `fetch + Authorization: Bearer ${CONSOLE_ACCESS_TOKEN}` 패턴은
  유지 가능하지만, secret 을 `cb_sk_` 로 교체 후 SDK 메서드로 옮기는 것을 권장 (만료 없음).

## [1.9.1] - 2026-04-25

문서 정합성 패치. 런타임 동작은 1.9.0 과 동일합니다.

### Fixed — `ConnectBaseConfig.persistence` jsdoc 모순

- `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' (권장·기본값)` 으로 올바르게 표기되어 있었음.
- IDE/타입 hint 가 잘못된 정보를 안내해 사용자가 새로고침 시 로그아웃되는 동작을 "버그" 로 오인할 수 있었음.
- 두 doc-comment 를 일치시켜 `@default 'none'` 으로 정정. 보안·XSS 메모도 함께 명시.
- **Behavior change 없음** — 기본 동작은 1.6.0 이후 줄곧 `'none'` 이었습니다.

## [1.9.0] - 2026-04-24

런타임 내구성·관측성 강화 릴리스. 공개 API 시그니처는 유지되며 모든 변경은 추가·하드닝 성격(비파괴).

### Added — 요청 타임아웃 & AbortSignal

- `ConnectBaseConfig.requestTimeoutMs` — 모든 HTTP 호출의 기본 타임아웃(ms). 기본 30000ms, 0/음수 시 비활성.
- 개별 호출에서 `AbortOptions` (`signal`, `timeout`) 지원 — Storage presigned 업로드 등 장시간 호출을 외부에서 취소 가능.
- `core/abort.ts`: `createTimeoutController`, `DEFAULT_REQUEST_TIMEOUT_MS` 유틸.

### Added — 전역 에러 관찰 훅

- `ConnectBaseConfig.onError` — 모든 `ApiError` / `AuthError` 발생 시 호출되는 옵저버. Sentry/Datadog 등 관측성 파이프라인 연결용.
- 429 응답 개선: `Retry-After` 헤더(초 또는 HTTP-date)를 파싱해 `ApiError.details.retry_after_seconds` 로 전달.

### Added — 응답 shape 검증 (fail-fast)

- `core/validate.ts`: `assertShape` 도입. 서버가 필수 필드를 누락한 응답을 반환해도 즉시 throw 하여 이후 로직이 `undefined` 로 조용히 실패하지 않게 방어.
- 적용 지점: `auth.signInMember`, `auth.getMe`, `payment.createCheckoutSession`, `push.registerDevice`, `queue.consume`, `subscription.create`.

### Added — Presigned URL 스킴 검증 (SSRF 하드닝)

- `core/url-validation.ts`: `validateExternalUrl`, `isLocalhostOrigin`.
- `StorageAPI` presigned PUT 호출 전에 URL 스킴(https)을 검증해 서버 응답을 맹신하는 경로를 차단. localhost 허용 여부는 런타임 오리진 기반으로 자동 결정.

### Changed — 에러 타입 통일

- `FunctionsAPI.invokeAndWait`, `GameAPI.listRooms` / `getRoom` — `throw new Error(...)` → `throw new ApiError(status, message, code)`. `ApiError` 는 `Error` 를 상속하므로 `instanceof Error` / `.message` 로 잡던 기존 코드는 그대로 동작하며, 추가로 `error.code` / `error.statusCode` 로 분기 가능.
- `VideoAPI` 내부 `request<T>()` — 서버의 구조화 응답 `{ error: { code, message, details } }` 을 완전히 언랩해 `ApiError(status, message, code, details)` 로 변환. timeout 도 함께 적용.

### Changed — Refresh 토큰 회복성

- `HttpClient` 에 지수 백오프 도입 — 연속 refresh 실패 시 500ms × 2^n (최대 30s) 동안 재요청을 차단해 서버에 실패 요청이 쏟아지는 회귀를 막는다. 성공 시 카운터 리셋.
- Refresh 요청 자체도 `requestTimeoutMs` 로 제한.

### Changed — 로깅

- `RealtimeAPI`: 내부 `console.error` 를 `private logError()` 로 통일. `options.debug` opt-in 일 때만 출력되어 프로덕션 devtools 에 SDK 로그 노출이 0.

### Docs

- `AnalyticsAPI.flush` / `getSession`, `VideoAPI.list` / `getStreamUrl` 에 상세 JSDoc + `@example` 추가.

### Compatibility

- **Non-breaking**: 모든 공개 메서드 시그니처 동일. `Error` → `ApiError` 전환은 상속 관계상 기존 `catch (e: Error)` 경로에 영향 없음.
- **Runtime behavior change**: 서버가 필수 응답 필드를 누락하는 경우, 이전 버전은 `undefined` 로 조용히 진행했지만 1.9.0 은 즉시 throw 한다. 서버 응답이 정상이라면 차이 없음.

## [1.8.1] - 2026-04-23

문서 정합성 패치. SDK 런타임 동작은 1.8.0 과 동일합니다.

### Changed — README

- `createData` 예제에 `DataItem` 반환 (id/created_at/updated_at 즉시 사용 가능) 설명 추가.
- `createMany` 예제 신설 — 반환 shape `{ created: DataItem[], total, success }` 명시.
- `updateData` 예제를 반환값을 받아 사용하는 형태로 정비.

## [1.8.0] - 2026-04-19

웹 스토리지 CLI 배포(`connectbase deploy ./dist`) 를 manifest 기반 **증분 업로드** 로 재구성. 동일 dist 재배포는 거의 즉시 완료되고, 일부만 바뀐 경우 변경분만 전송한다. 서버 DB 쓰기도 단일 트랜잭션 + bulk insert 로 묶어 전체 배포 시간이 짧아짐.

### Added — 증분 배포

- **`GET /v1/public/storages/webs/:storageID/deploy/manifest`** — 현재 배포된 파일의 `path/size/hash(sha256)` 와 결정적 `revision` 반환.
- **`POST /v1/public/storages/webs/:storageID/deploy/incremental`** — `{ upsert, delete, base_revision? }` 변경분만 적용. 서버가 hash 를 재검증하고 단일 트랜잭션으로 upsert/delete 처리 후 기존 배포 파이프라인으로 Object Storage 에 전체 업로드.
- CLI 는 prod 배포 시 자동으로 manifest → diff → incremental 순서를 사용한다. 변경 없음이면 `✓ 변경사항 없음` 출력 후 종료.
- `base_revision` 은 옵션. 동시 배포 경합에서 `409 Conflict` 면 manifest 재조회 후 1회 자동 재시도.

### Changed — 서버 배포 속도 개선 (기존 `/deploy` 포함)

- `CLIDeploy` 가 파일/폴더별 개별 트랜잭션을 단일 트랜잭션 + depth-batch `CreateBulk` 로 교체. 수십~수백 개의 `BEGIN/COMMIT` 왕복이 1회로 축소.
- 실패 시 기존 파일도 함께 롤백되어 **실패한 배포가 스토리지를 비우는 회귀가 사라짐** (과거엔 DeleteAll 이 선행되고 이후 SaveFile 이 실패하면 스토리지가 빈 상태로 남았음).

### Compatibility

- **구버전 서버**: manifest 엔드포인트 404 → 기존 `/deploy` 전량 업로드로 자동 폴백.
- **구버전 SDK** (`1.7.x` 이하): 기존 `/deploy` 는 그대로 동작 (내부 로직만 최적화됨).
- **Dev 배포** (`connectbase deploy --dev`) 는 Object Storage 에 직접 업로드하는 구조상 증분 적용 불가 — 항상 전량 업로드 유지.

### Internal — DB 스키마

- `storage_web_file.content_hash` 컬럼 추가 (Optional, Default `""`). 기존 row 는 manifest 최초 조회 시 lazy backfill.

## [1.7.0] - 2026-04-19

콘솔(JWT) DB 관리 API 경로 정정 — 기존 메서드들이 실제로 존재하지 않는 라우트(`/v1/apps/:appID/tables/:tableID/...`, `/v1/apps/:appID/triggers`, `/v1/apps/:appID/security/rules`, `/v1/apps/:appID/tables/:tableID/relations`) 를 호출해 서버가 항상 404 를 반환하던 문제를 수정.

### Fixed — 404 를 반환하던 메서드들이 이제 정상 동작

실제 백엔드 라우트는 모두 `/v1/apps/:appID/databases/...` prefix 아래에 있습니다 ([route/api/v1.go:617-679](../../../backend/cmd/core-server/app/route/api/v1.go#L617)). SDK 가 이 prefix 를 빠뜨리고 있었습니다.

- **`database.listIndexes` / `createIndex` / `deleteIndex` / `analyzeIndexes`** — 경로 `/v1/apps/:appID/tables/:tableID/indexes*` → `/v1/apps/:appID/databases/tables/:tableID/indexes*`
- **`database.listSearchIndexes` / `createSearchIndex` / `deleteSearchIndex`** — 동일 패턴 수정
- **`database.listGeoIndexes` / `createGeoIndex` / `deleteGeoIndex`** — 동일 패턴 수정
- **`database.listTriggers` / `createTrigger` / `updateTrigger` / `deleteTrigger`** — 경로 `/v1/apps/:appID/triggers*` → `/v1/apps/:appID/databases/triggers*`
- **`database.listSecurityRules` / `createSecurityRule` / `updateSecurityRule` / `deleteSecurityRule`** — 경로 `/v1/apps/:appID/security/rules*` → `/v1/apps/:appID/databases/security/rules*`

### Breaking — relations API 시그니처 변경

실제 백엔드 `relations` 라우트는 app 전역(`/v1/apps/:appID/databases/relations`) 에 있고 관계는 UUID(`relation_id`) 로 식별됩니다. 기존 SDK 시그니처는 존재하지 않는 table-scoped 경로 + 관계 이름으로 삭제를 시도해 호출 자체가 불가능한 상태였습니다.

- **`database.listRelations(appId, tableId)` → `database.listRelations(appId, sourceTable?)`** — 두 번째 인자는 필터용(선택), 생략 시 앱 전체 관계 반환. `sourceTable` 은 query string `?source_table=` 으로 전달됩니다.
- **`database.createRelation(appId, tableId, data)` → `database.createRelation(appId, data)`** — `tableId` 인자 제거. 테이블 정보는 body 의 `source_table`/`target_table` 로 전달.
- **`database.deleteRelation(appId, tableId, relationName)` → `database.deleteRelation(appId, relationId)`** — `relationId` 는 `listRelations` 가 반환하는 UUID (`id` 필드). 기존 `(tableName, alias)` 조합으로 지정할 수 없습니다.

### Migration

기존 호출이 실제로 성공하는 경로가 없었으므로(항상 404) 깨지는 호출자는 없어야 합니다. relations 를 쓰던 소비자는 위 새 시그니처로 교체하고, triggers/indexes/security-rules 는 코드 변경 없이 자동으로 동작하게 됩니다.

## [1.6.0] - 2026-04-19

웹 방문 추적에 로그인 회원을 자동 연결 — 콘솔의 **앱 멤버 > 활동기록 > 웹방문기록** 탭이 이제 실제로 채워진다. 이전까지는 SDK 가 배치 이벤트를 `visitor_uid` 만 실어 보내 `web_visitors.app_member_id` 가 전부 NULL 로 남아 있었다.

### Added

- **`analytics.setMemberId(id | null)`**: 로그인/로그아웃 시점에 방문자 트래커에 회원 ID 를 전달한다. 이후 모든 페이지뷰/이벤트 배치 (`/v1/public/storages/web/{id}/visitors/batch`) 요청에 `app_member_id` 필드가 포함되어 서버가 게스트 방문자를 회원과 자동 연결한다.
- **`analytics.getMemberId()`**: 현재 트래커에 설정된 회원 ID 조회. 로그인 상태 확인용.

### Changed

- **`auth.signUpMember/signInMember/signInAsGuestMember` 가 내부적으로 `analytics.setMemberId()` 호출**: 기존 `window.__cbSetMember` 전역 콜백만 호출하던 경로가 실제로 배치 큐에 회원 ID 를 적용하도록 연결됨. `auth.signOut()` 시에는 `setMemberId(null)` 로 익명 상태 복귀. `ConnectBase` 생성자에서 `auth._attachAnalytics(analytics)` 로 자동 연결.
- **`analytics.identify(memberId)` 가 `setMemberId` 로 단순화**: 동작 동일하나 내부 구현이 새 경로로 통합. 기존 호출부는 그대로 동작.

### Why

기존 구현은 `auth.ts` 가 `window.__cbSetMember(memberId)` 를 호출하면 "끝" 이었는데, `AnalyticsAPI` 에는 이 전역 함수를 받을 경로도 없고 로그인 후 쌓이는 배치 이벤트에도 회원 ID 가 첨부되지 않았다. 그 결과 `web_visitors.app_member_id` 가 항상 NULL 이라 앱 멤버 페이지의 방문기록 탭이 텅 비어 있었다. 이제 로그인이 일어나면 그 시점부터의 모든 이벤트 배치에 `app_member_id` 가 자동으로 따라간다.

## [1.5.0] - 2026-04-19

터널의 proxy_token UX 정리 — 콘솔 AI Config 경로(95%+ 사용자)는 토큰을 건드리지 않고, curl/웹훅처럼 외부에서 터널 URL 을 직접 호출하는 소수 케이스에만 명시적 플래그로 opt-in 하도록 분리.

### Added

- **`--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`).
- **`--show-token` 플래그**: proxy_token 값과 `curl -H "X-Proxy-Token: ..."` / `?proxy_token=` 예시를 CLI 에 출력. curl 로 터널을 직접 때려보고 싶을 때만 사용.
- **감사 로그·메트릭**: 공개 모드 터널 세션 생성 시 `tunnel_public_opened_total{app_id}` 카운터 증가 + 매 요청마다 remote IP/method/path 가 `Public tunnel access` 로그로 남음.

### Changed

- **터널 기동 시 proxy_token 기본 숨김**: 1.4.1 에서 추가했던 "토큰 + curl 예시 자동 출력"을 제거. 기본 터널 사용자는 콘솔 AI Config 가 서버에서 자동으로 토큰을 resolve 하므로 CLI 에 노출할 필요가 없다. 필요하면 `--show-token` 으로 명시적으로 꺼냄.
- **`tunnel_ready` 프로토콜 메시지에 `public` 필드 추가**: 서버가 CLI 에 현재 세션의 공개 여부를 내려주어 CLI 가 올바른 안내를 출력하도록 함 (`protocol/message.go`).

### Why

(1) 1.4.1 의 토큰 자동 출력은 기본 플로우(콘솔 AI Config)에서는 "내가 뭘 해야 하나?" 혼란을 유발했다. 대부분 사용자는 토큰 존재조차 모르는 게 맞다. (2) Stripe/GitHub 처럼 커스텀 헤더를 못 실어 보내는 웹훅 수신처는 `?proxy_token=` 쿼리 번거로움 + 세션 재시작 시 재등록 문제가 있었다. `--public` 으로 opt-in 할 수 있게 해 이 케이스를 깔끔히 분리.

## [1.4.2] - 2026-04-18

`npx connectbase docs` 명령이 init/deploy/tunnel 과 달리 brower auth 흐름을 거치지 않아, Public Key 가 없는 사용자가 키 발급 경로를 모른 채 prompt 만 보던 UX 문제 해결.

### Fixed

- **`docs` 가 키 없을 때 자동 발급 흐름을 트리거**: `.connectbaserc.publicKey` → `secretKey` 가 있으면 앱 선택만, 없으면 `browserAuthFlow()` → 앱 선택/생성 → Public Key 신규 발급 → `.connectbaserc` 저장까지 자동 수행. 한 번 실행하면 다음부터는 추가 입력 없이 통과 (`cli.ts` `ensureDocsPublicKey`).
- **`tunnelAppId` 캐시 공유**: docs 와 tunnel 이 동일한 `.connectbaserc.tunnelAppId` 를 사용하므로 한쪽에서 앱을 선택하면 다른 쪽에서도 재사용된다.
- **죽은 fallback 정리**: `config.publicKey ?? config.publicKey` (자기 자신 fallback), `!config.publicKey && !config.publicKey` (동일 변수 두 번 검사) 제거.

### Internal

- `resolveAppForTunnel` → `resolveApp` 으로 일반화. 새 앱 생성 시 백엔드(`POST /v1/public/cli/apps`) 응답에 포함된 `public_key` 를 같이 반환하도록 시그니처 확장. tunnel 호출부는 동작 동일.

## [1.4.1] - 2026-04-18

`connectbase tunnel` CLI 가 공개 URL 호출에 필요한 **proxy token** 을 표시하지 않아, 사용자가 토큰을 알 방법이 없어 모든 요청이 `401 invalid or missing proxy token` 으로 막히던 문제 해결.

### Fixed

- **`tunnel_ready` 출력에 proxy token 노출**: 터널 활성화 시 서버가 내려보낸 세션 단위 토큰을 stdout 에 표시합니다 (`cli.ts` `handleMessage`). 이전에는 `tunnel_handler.go:300-308` 가 `proxy_token` 을 보내주지만 CLI 가 무시해서 사용자가 토큰을 확인할 경로가 사실상 없었습니다.
- **사용 예시 안내**: 공개 URL 호출 시 사용해야 하는 헤더(`X-Proxy-Token`)와 쿼리(`?proxy_token=`) 형식을 `curl` 예시로 함께 출력합니다. 백엔드 `proxy_handler.go:231-239` 가 검증하는 입력은 이 두 가지뿐이며 `Authorization`, `Cookie`, Basic Auth 는 받지 않습니다.

### Notes

- 토큰은 터널 세션 단위로 서버가 새로 생성합니다. 세션 종료(WebSocket disconnect) 시 즉시 무효화되므로 stdout 노출에 따른 추가 위험은 거의 없습니다.
- 백엔드 / 콘솔 / 문서 측 변경은 없습니다 — 클라이언트 표시만 보강.

## [1.4.0] - 2026-04-18

`FetchDataResponse` 타입을 실제 서버 wire 포맷에 맞추는 타입 정정.

### Fixed

- **`FetchDataResponse` 필드명을 서버 응답에 맞춤**: `datas` → `data`, `total_size` → `total_count`.
  - 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` 를 받고 있었습니다.
  - `0.16.0` 에서 한 번 바로잡았다가 `0.16.1` 에서 "서버 wire 가 여전히 `datas`" 라는 잘못된 관찰로 롤백됐던 이슈를 다시 수정합니다. 서버 핸들러 (`gin.H{"data": ..., "total_count": ...}`) 를 직접 확인해 타입을 확정했습니다.

### Breaking (types only, runtime unchanged)

- `FetchDataResponse` 의 필드명이 TypeScript 레벨에서 변경됩니다. 런타임 동작(실제 네트워크 응답)은 동일합니다. 기존에 타입 경고를 무시하고 `result.data` / `result.total_count` 로 접근하던 코드는 **수정 없이 바로 동작**합니다. 반대로 타입 정의를 믿고 `result.datas` / `result.total_size` 로 접근하던 코드는 원래부터 런타임에 `undefined` 를 받고 있었으므로, 이번 기회에 `result.data` / `result.total_count` 로 고쳐주세요.

### Migration

```ts
// Before (타입은 OK 였지만 런타임 undefined)
const { datas, total_size } = await cb.database.getData(tableId)

// After (v1.4.0+)
const { data, total_count } = await cb.database.getData(tableId)
```

## [1.3.0] - 2026-04-18

파티 초대 수락/거절 SDK 메서드 추가. 기존 `acceptInvite` 는 로비 전용(`/lobbies/invites/...`) 이라 파티 초대에는 사용할 수 없었는데, 1.2.0 까지는 사용자가 직접 `fetch()` 로 백엔드의 `/v1/game/:appID/invites/:inviteID/accept` 를 호출해야 했음. 1.3.0 부터 SDK 에 전용 메서드 제공.

### Added

- **Party**: `cb.game.acceptPartyInvite(inviteId, playerId, displayName?)` — `inviteToParty` 로 생성된 초대를 수락하여 파티 합류. 백엔드 엔드포인트 `POST /v1/game/:appID/invites/:inviteID/accept` (query string 으로 `player_id`, `display_name` 전달).
- **Party**: `cb.game.declinePartyInvite(inviteId, playerId)` — 파티 초대 거절.

### Changed

- `cb.game.joinParty` throw 메시지를 `acceptPartyInvite` 플로우 안내로 갱신 (기존 `acceptInvite` 권고는 잘못된 안내였음 — 해당 메서드는 로비 전용).

## [1.2.0] - 2026-04-18

Replay SDK 4개 메서드 활성화. 백엔드가 `REPLAY_STORAGE_PATH` 환경변수로 replay 저장소를 구성한 경우에만 동작하며, 미설정 시 SDK 가 명시적 에러를 throw (404 분기).

### Added

- **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).` 형태의 명확한 에러 발생.

## [1.1.0] - 2026-04-18

game-server 의 party/spectator/ranking/voice public 라우트 오픈에 맞춰 SDK 의 throw 해제. 백엔드 미제공 기능은 여전히 throw 로 남김.

### Added

- **Party**: `cb.game.createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화 (backend `/v1/game/:appID/parties/...`). `joinParty` 는 백엔드에 직접 join 엔드포인트가 없어서 `inviteToParty` → `acceptInvite` 플로우로 대체 — `joinParty` 호출 시 해당 안내 Error throw.
- **Spectator**: `cb.game.joinSpectator/leaveSpectator/getSpectators` 활성화 (backend `/v1/game/:appID/rooms/:roomID/spectators`).
- **Ranking**: `cb.game.getLeaderboard/getPlayerStats/getPlayerRank` 활성화. **시그니처 변경** — 백엔드가 `game_type` 쿼리를 필수로 요구하므로 SDK 에서도 첫 인자로 `gameType` 을 받음. `getLeaderboard(gameType, top?, season?)`, `getPlayerStats(playerId, gameType, season?)`, `getPlayerRank(playerId, gameType, season?)`.
- **Voice**: `cb.game.joinVoiceChannel` 활성화 (backend `/v1/game/:appID/voice/rooms/:roomID/join`).

### Still disabled (throws Error — 백엔드 public 경로 미제공)

- `cb.game.createRoom`, `cb.game.deleteRoom` — 콘솔 전용
- `cb.game.joinParty` — 초대 수락 플로우 사용
- `cb.game.listReplays`, `getReplay`, `downloadReplay`, `getReplayHighlights` — replay 스토리지 아직 미연결

## [1.0.0] - 2026-04-18

문서-코드 정합성 9라운드 감사 후속. 런타임 동작이 실제 백엔드와 일치하지 않던 메서드들을 정정. `GoogleConnectionStatus` shape 변경은 breaking change 이므로 major bump.

### Breaking

- **`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.
- **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 경로 오픈 요청 필요.

### Fixed

- **`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.
- **`storage.moveFile()` / `renameFile()` Public Key 404 명시적 에러**: 해당 라우트는 `/v1/public/...` 에 노출되어 있지 않음. Public Key 만 있고 JWT 없을 때 호출하면 이제 즉시 명시적 Error. JWT 인증 시 `/v1/storages/...` 로 정상 호출.

### Changed

- `HttpClient.hasJWT()` 메서드 추가 — Access Token(JWT) 존재 여부 확인용.
- `payment.prepare()` JSDoc 예제를 Toss Payments V2 시그니처(`payment.requestPayment({ method, amount: { currency, value }, ... })`) 로 갱신.

### Documentation

- `KnowledgeSearchRequest`, `ImportDataRequest` 타입에 JSDoc `@example` 추가 — IDE 자동완성 툴팁 보강.

## [0.16.1] - 2026-04-17

MCP/SDK 문서-코드 정합성 검증에서 발견된 불일치 소규모 정정. 런타임 동작 변경은 없으며, 이전 릴리스에서 도입된 타입 회귀를 롤백.

### Fixed

- **`FetchDataResponse` 필드명 회귀 롤백**: `0.16.0` 에서 `datas→data`, `total_size→total_count` 로 바꿨으나 실제 서버 wire 는 여전히 `datas` / `total_size` 였음. 타입 정의를 서버 응답 그대로 `{ datas, total_size }` 로 되돌려 런타임 미스매치 제거.
- `cb.push.subscribeTopic()` JSDoc 예제가 `device_token` 인자 누락된 구 시그니처로 표기되던 문제 정정 (실 시그니처는 `subscribeTopic(deviceToken, topic)`).
- CLI 안내 문구·`setupMonorepoRoot` 로 추가되는 `cb:update`/`cb:docs`/`cb:mcp` 스크립트가 `connectbase-client` 를 가리키던 것을 정규 bin 이름인 `connectbase` 로 통일. 두 bin 은 모두 동작하지만 문서와 자동 생성 스크립트는 단일 이름 기준으로 일관화.

### Documentation

- README `### Push Notifications` 섹션의 예제를 실제 공개 API 와 일치하도록 수정: `push.register()` / `push.subscribeToTopic()` / `push.unsubscribeFromTopic()` (존재하지 않는 메서드) → `push.registerDevice()` / `push.subscribeTopic(deviceToken, topic)` / `push.unsubscribeTopic(deviceToken, topic)` / Web Push 등록 예제 추가.

### Changed

- `package.json` 메타데이터 보강: `author`, `homepage`, `bugs`, `sideEffects: false` 필드 추가, `files` 에 `LICENSE`/`CHANGELOG.md`/`README.md` 명시적 포함.
- 저장소 루트에 `LICENSE` (MIT) 파일 신설 (이전에는 `license: "MIT"` 선언만 존재).

## [0.16.0] - 2026-04-17

CLI `update` 커맨드와 신규 저장소 API 옵션, 토큰 영속성 옵션 추가. 일부 타입 정정.

### Added

- **CLI `update` 커맨드**: 현재 설치된 `connectbase-client` 버전을 npm 과 비교하고 `docs`·`mcp` 산출물을 일괄 최신화.
- **모노레포 지원**: `connectbase init --setup-root` 로 모노레포 루트 `package.json` 에 `cb:update`/`cb:docs`/`cb:mcp` 편의 스크립트 설치.
- **Storage `getFiles()` `parentId` 옵션**: 특정 폴더 하위 파일 목록 조회 지원.
- **토큰 영속성(persistence) 옵션**: `new ConnectBase({ persistence: 'localStorage' | 'sessionStorage' | 'none' })`.
  - `localStorage` (기본값): 브라우저 종료 후에도 토큰 유지.
  - `sessionStorage`: 탭 종료 시 삭제.
  - `none`: 메모리에만 저장.
  - 생성자에서 저장된 토큰 자동 복원 (명시적 토큰 전달 시 스킵). SSR 환경(`window` undefined) 안전 처리.
- **OAuthProvider 확장**: `kakao`, `apple` 프로바이더 추가 (서버 enum 과 동기화).

### Changed

- `VERSION` 상수가 `package.json` 에서 동적으로 로드되도록 변경 (이전: `0.10.6` 하드코딩).
- `FetchDataResponse` 필드명 변경: `datas` → `data`, `total_size` → `total_count` (서버 응답과 일치를 의도함).
  - **⚠️ 본 변경은 실제 서버 wire 와 맞지 않는 회귀였으며 `0.16.1` 에서 롤백됩니다.** `0.16.0` 사용자는 `0.16.1` 이상으로 즉시 업그레이드 권장.

### Documentation

- SDK 문서 전체 감사 및 누락 섹션 보강.

## [0.15.0] - 2026-04-16

Web Analytics SDK 모듈 신설 및 터널 유틸 리팩터.

### Added

- **AnalyticsAPI** 모듈 (`cb.analytics`): `init` / `trackEvent` / `trackPageView` / `identify` / `setConsent` / `enableHeatmap` / `enableHeartbeat` / `destroy`.
- **SessionManager**: 30분 타임아웃, `visitor_uid` localStorage 영속화.
- 백엔드 측 12개 서비스·9개 Ent 스키마(이벤트/퍼널/세션/코호트/어트리뷰션/세그먼트/A-B 테스트/히트맵/녹화/프라이버시) 와 매칭.

### Changed

- CLI 내부에서 tunnel 관련 로직을 `src/tunnel-utils.ts` 로 추출 (테스트 용이성 확보).
- `vitest` 도입 및 tunnel/analytics 단위 테스트 23건 추가.

## [0.14.0] - 2026-04-15

Tunnel 안정성 개선.

### Added

- **Tunnel lockfile**: 앱+포트 기반 lockfile 로 동일 터널 중복 실행을 차단. stale lockfile 자동 감지(PID 생존 확인).
- `--force` 플래그: lockfile 무시 옵션.

### Changed

- 서버의 `tunnel_error code=replaced` 수신 시 재연결을 중단하고 안내 메시지 출력 (이전에는 끊임없이 재연결 시도).
- `TunnelMessage` 타입에 `code` / `error` / `message` 필드 추가.

## [0.13.0] - 2026-04-14

Database API 정합성 및 완성도 대규모 개선.

### Breaking Changes

- **`TableSchema` 응답 구조 전면 재정의**: 서버 ent 모델 (`backend/cmd/data-server/ent/schema/table.go`) 과 1:1 일치하도록 변경.
  - `name` → `title`
  - `columns: ColumnSchema[]` → `schema: TableSchemaDefinition` (평면 또는 중첩 맵)
  - `created_at` → `create_time` (※ G2 작업에서 다시 `created_at` 로 표준화 예정)
  - `updated_at` → `update_time`
  - 신규 필드: `app_id`, `access_level`, `is_active`, `validation_schema?`
- **`CreateTableRequest` 형태 변경**: 서버 DTO 와 일치.
  - `schema?: TableSchemaDefinition` 신규 필드. 평면 맵 (`{email: 'string'}`) 또는 중첩 객체 (`{email: {type: 'string', required: true}}`) 모두 지원. `$required` 키로 필수 컬럼 지정.
  - `accessLevel?: 'Creator' | 'Public' | 'AppMember'` (기본 `'Creator'`)
  - `description` 은 `@deprecated` — 서버에 저장되지 않음
- **`createTable()` 반환 타입**: `Promise<TableSchema>` → `Promise<void>`. 서버는 `{message}` 만 반환하므로 거짓 타입 제거.
- **`createColumn()` / `updateColumn()` 반환 타입**: 동일한 이유로 `Promise<void>` 로 변경.
- **`updateTable()` 시맨틱 정정**: PATCH 부분 업데이트가 정상 동작. 이전 버전에서는 모든 필드가 required 라 부분 업데이트 시 400 오류가 발생했음.
- **`DataType` 정렬**: 서버 `ValidSchemaTypes` 와 일치하도록 변경.
  - 제거: `'boolean'`
  - 추가: `'int'`, `'bool'`, `'uuid'`
  - 최종 union: `'string' | 'int' | 'number' | 'bool' | 'uuid' | 'date' | 'object' | 'array'`
- **`CreateColumnRequest`**:
  - `is_required` 가 optional 로 변경 (기본 false)
  - `default_value` 타입 `string` → `unknown` (서버는 임의 타입 허용)
  - `order`, `validation_rule` 은 `@deprecated` (서버 미지원)
- **`UpdateColumnRequest`**:
  - `default_value` 타입 `string` → `unknown`
  - `name` `@deprecated` (서버는 컬럼 rename 미지원)
  - `order`, `validation_rule` `@deprecated`
- **`ColumnSchema`**:
  - SDK 가 합성하는 객체임을 명시
  - `default_value` 타입 `string` → `unknown`
  - `created_at` 은 테이블의 `create_time` 으로 대체 (컬럼 개별 타임스탬프 없음)
  - `order` 는 SDK 가 부여한 인덱스
  - `updated_at`, `validation_rule` `@deprecated`

### Added

- `TableSchemaDefinition` interface — 컬럼 정의 맵 타입. 플랫 / 중첩 / `$required` 모두 지원.
- `TableColumnDef` union type — 단일 컬럼 정의 (플랫 string 또는 중첩 객체).
- `TableAccessLevel` type — `'Creator' | 'Public' | 'AppMember'`.
- `database.createTable()` 가 초기 schema 와 access level 을 한 번에 받아 1-step 생성 지원.
- `database.getColumns()` 가 플랫 / 중첩 schema 모두 정확히 파싱.
- `database` 모듈의 `getPublicPrefix()` 가 항상 `/v1/public` 반환 (이전: 인증 종류에 따라 `/v1/public` 또는 `/v1` — 후자는 dead path).
- `database.getValidationSchema()`, `setValidationSchema()`, `deleteValidationSchema()` — 테이블 검증 스키마 CRUD.
- `MemberInfoResponse` 에 `email?`, `role?` 필드 추가 — RLS `auth.email`/`auth.role` 과 일치.
- MCP `update_column` 툴 신설 (description, encrypted, data_type, is_required, default_value 변경).
- MCP `get_validation_schema`, `set_validation_schema`, `delete_validation_schema` 툴 신설.

### Fixed

- **🔴 `database.createTable()` 가 어떤 키로도 동작하지 않던 broken 상태 해결**.
  - Public Key 경로: 요청 body 가 서버 DTO 와 불일치 (필드명 `name`/`title`, 누락 `access_level`, 빈 `schema`)
  - Secret Key 경로: 존재하지 않는 `/v1/tables` 경로 호출 → 404
  - 두 경로 모두 본 릴리스에서 정상 동작.
- **MCP `create_table` 툴이 빈 schema 거부로 항상 실패** 하던 문제 해결 (서버 hook 이 explicit 빈 맵을 거부했음). 서버 DTO/repository/MCP tool 세 곳을 동시에 수정.
- 서버 측 `CreateTableRequest.Schema` 가 `binding:"required"` 였으나 schema 생략 가능하도록 변경.
- 서버 측 `EditTableInternal` (PATCH) 가 full-replace 시맨틱이라 부분 업데이트 불가능했던 문제 해결. `PatchTableRequest` DTO 와 `PatchTable` repository 메서드 신설.
- MCP `create_column`, `update_column` 툴의 `data_type` enum 이 `'boolean'` 을 포함했으나 서버는 `'bool'` 만 허용 → 정정.
- `getColumns()` 가 플랫 schema (`"email": "string"`) 를 모두 `'string'` 으로 잘못 반환하던 문제 해결.

### Documentation

- `secretKey` 의 JSDoc 명확화: 앱 DB API 에는 사용 불가, CLI / tunnel 전용임을 명시.
- embedded SDK 문서 (04-sdk-database.md, 11-mcp-tools.md, 25-app-packaging.md) 의 `boolean` → `bool`, `auth.email`/`auth.role` 정합성 정리, `createTable` 예제 갱신.
- `TableSchemaDefinition` 의 `$required` 키 사용법, `TableColumnDef` 평면/중첩 형태 모두 JSDoc 예제 포함.

### Notes

본 변경의 대부분은 backwards-incompat 이지만, 이전 버전 (`0.12.x`) 의 `createTable` 자체가 broken 상태였으므로 해당 메서드를 사용하던 코드는 어차피 동작하지 않았습니다. read 메서드 (`getTables`, `getTable`, `getColumns`) 는 사용자 코드가 escape-hatch 캐스팅 (`as any`) 으로 우회하던 경우만 영향받습니다.

---

## [0.12.2] - 2026-04-?? (이전)

이전 릴리스 — 본 CHANGELOG 도입 이전. git history 참조.
