connectbase-client 6.10.0 → 6.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -0
- package/README.md +20 -1
- package/dist/cli.js +230 -35
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +410 -75
- package/dist/index.d.ts +410 -75
- package/dist/index.js +11213 -10261
- package/dist/index.mjs +11213 -10261
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,134 @@
|
|
|
3
3
|
본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
|
|
4
4
|
버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
|
|
5
5
|
|
|
6
|
+
## [6.12.0] - 2026-11-11
|
|
7
|
+
|
|
8
|
+
### Changed — 세션이 없다는 것을 이미 알면 `getMe()` 가 서버에 묻지 않는다
|
|
9
|
+
|
|
10
|
+
- 메모리에 토큰이 없고 60초 안의 쿠키 복구가 "세션 없음"(401 또는 quiet 204)으로 끝났으면, `cb.auth.getMe()` 는 요청 없이
|
|
11
|
+
서버가 주는 것과 같은 401 `ApiError` 를 던진다. 반환과 오류 계약은 그대로이고, 로그인 안 한 방문자에게 매 페이지
|
|
12
|
+
「Failed to load resource 401」(`/v1/public/app-members/me`)이 남던 것만 사라진다.
|
|
13
|
+
|
|
14
|
+
### Security — 소셜 로그인 콜백은 세션을 만들지 않는다. 이 브라우저에서 시작한 로그인임을 확인한 교환만 만든다
|
|
15
|
+
|
|
16
|
+
6.11.0 은 남이 보낸 콜백 링크의 URL 토큰과 `code` 를 거부했지만, 서버가 콜백 응답에서 이미 refresh 쿠키를 심은 뒤였다.
|
|
17
|
+
공격자가 자기 계정으로 시작해 멈춰 둔 프로바이더 콜백 링크(`/v1/public/oauth/<provider>/callback?code=&state=`)를 피해자가
|
|
18
|
+
열면, SDK 가 결과를 거부해도 다음 페이지에서 그 쿠키로 공격자 세션이 복구됐다(로그인 CSRF).
|
|
19
|
+
|
|
20
|
+
- `cb.oauth.signIn()` / `signUp()` / `signInWithPopup()`(네이티브 브릿지 포함)이 인가 요청에 `response_type=code` 를 싣는다.
|
|
21
|
+
서버는 그 흐름의 콜백에 토큰 대신 1회용 `code` 만 싣고 쿠키를 심지 않는다. 세션 쿠키는 SDK 가 state 를 확인한 뒤 부르는
|
|
22
|
+
`POST /v1/auth/oauth/exchange` 응답이 심는다(서버는 그 앱 소유 origin 의 요청에만 심는다).
|
|
23
|
+
- `getCallbackResult()` 와 부팅 안전망(`new ConnectBase()`)이 `?code=&state=` 콜백을 처리한다. state 가 이 브라우저에서
|
|
24
|
+
만든 값이면 교환해 토큰을 저장하고 결과는 예전과 같은 모양(`access_token`, `refresh_token`, `member_id`, `is_new_member`,
|
|
25
|
+
`email`, `state`)이다. 콜백 페이지에서 `getCallbackResult()` 만 부르던 앱은 코드를 바꿀 필요가 없다. `code` 를 한 번만
|
|
26
|
+
교환하므로 `exchangeCodeFromCallback() ?? getCallbackResult()` 패턴도 그대로 동작한다.
|
|
27
|
+
- state 가 맞지 않는 `code` 는 교환하지 않는다. `getCallbackResult()` 는 이제 `null` 대신 `{ error: 'oauth_state_mismatch' }`
|
|
28
|
+
를 돌려주고, 주소의 `code` 는 건드리지 않는다(앱 자체 OAuth 의 `code` 일 수 있다). 부팅 때는 그런 `code` 를 손대지 않는다.
|
|
29
|
+
- 앱이 지정한 팝업 콜백 페이지(`signInWithPopup(provider, callbackUrl)`)도 `code` 를 받는다. 그 페이지의 `getCallbackResult()`
|
|
30
|
+
가 교환해 같은 출처 opener 에게 넘기고 창을 닫는다.
|
|
31
|
+
- 네이티브 브릿지 팝업(`signInWithPopup` 이 패키징 앱에서 외부 브라우저로 가는 경로)도 일회용 state 를 보내고, 돌아온 주소의
|
|
32
|
+
state 가 맞을 때만 결과를 받는다. 같은 스킴으로 열린 남의 주소가 결과로 넘어와도 로그인되지 않는다.
|
|
33
|
+
- 서버가 `response_type` 을 모르면(옛 서버) 예전처럼 URL 토큰을 받아 처리한다. 서버와 SDK 의 배포 순서는 상관없다.
|
|
34
|
+
|
|
35
|
+
### Security — 결제 금액은 서버의 가격표가 정한다
|
|
36
|
+
|
|
37
|
+
- `cb.payment.prepare({ product_id, quantity })` 형태를 더한다. 콘솔(결제 설정 > 가격표)에 등록한 상품의 단가 x 수량과
|
|
38
|
+
통화를 **서버가** 계산한다. 브라우저에서 `amount` 를 보내면 사용자가 개발자 도구로 10,000원 상품을 1원으로 고쳐 결제할
|
|
39
|
+
수 있었다. `createCheckoutSession({ product_id, quantity, ... })` 도 같다.
|
|
40
|
+
- 금액을 직접 보내는 기존 형태(`amount` + `order_name`)는 그대로 동작한다. 다만 서버는 결제 설정의 금액 출처가
|
|
41
|
+
`client`(레거시, 기존 앱의 기본값)인 앱이거나 서버 호출(시크릿 키, service_role 함수)일 때만 받는다. 새 앱은
|
|
42
|
+
`catalog` 로 시작하고, 이 앱에서 브라우저가 금액을 보내면 403 `client_amount_not_allowed` 다.
|
|
43
|
+
- 두 형태를 섞으면(`product_id` 와 `amount`/`order_name`) 컴파일 에러다(`PreparePaymentRequest` 는 이제
|
|
44
|
+
`PreparePaymentWithProduct | PreparePaymentWithAmount` 유니온이다). 서버도 400 `product_with_amount_fields` 로 거절한다.
|
|
45
|
+
- 응답에 `currency`(결제 금액의 통화)와 `product_id`, `quantity` 를 싣는다. `confirm()` 응답에도 `currency` 가 오고,
|
|
46
|
+
`getByOrderId()`/`list()` 결과에도 `product_id`, `quantity` 가 온다. 권한 지급은 `metadata`(클라이언트 값)가 아니라
|
|
47
|
+
이 두 값으로 한다.
|
|
48
|
+
- 새 오류 코드: `payment_product_not_found`(404), `payment_product_inactive`(409), `product_with_amount_fields`,
|
|
49
|
+
`product_or_amount_required`, `quantity_requires_product`, `payment_amount_too_large`, `currency_not_supported`(400,
|
|
50
|
+
toss 와 payapp 은 KRW 상품만), `client_amount_not_allowed`(403).
|
|
51
|
+
|
|
52
|
+
### Added — 로그인 멤버의 실시간 연결이 만료 전에 토큰을 갱신한다 (`cb.realtime`, `cb.database` 실시간)
|
|
53
|
+
|
|
54
|
+
- 서버(socket-server, data-server)가 멤버 토큰 연결의 만료를 집행한다. 연결 토큰이 이 SDK 가 로그인으로 들고 있는 멤버
|
|
55
|
+
세션(같은 앱, 같은 멤버)의 것이면 SDK 가 `token_refresh=1` 로 협상하고, 서버가 만료 5분 전에 보내는 `token_expiring`
|
|
56
|
+
(또는 `token_expired`)에 맞춰 HTTP 요청과 같은 refresh(같은 single-flight, 같은 실패 처리)로 새 토큰을 받아 연결 위에서
|
|
57
|
+
바꾼다(`auth_refresh`). 구독, 프레전스, DB 스냅샷을 다시 받지 않는다. 실패하면 30초마다 다시 해 본다.
|
|
58
|
+
- 재연결은 붙기 전에 토큰을 확인해, SDK 세션의 토큰이 1분 안에 만료되면 새로 받아 붙는다(끊긴 사이 만료된 토큰으로
|
|
59
|
+
재연결이 거부되던 문제). 갱신한 토큰은 이후 재연결에도 쓴다. `connectRealtime()` 은 넘긴 옵션 객체를 고치지 않는다.
|
|
60
|
+
- 갱신할 수 없는 토큰(다른 곳에서 받은 토큰, 함수 런타임의 위임 토큰, cross-app OAuth 토큰)은 협상하지 않는다 — 예전과
|
|
61
|
+
같이 서버가 만료 24시간 뒤에 닫는다(`code: 'token_expired'`, close 4401).
|
|
62
|
+
- `HttpClient` 에 `canRefreshMemberToken(token)`, `freshTokenForMember(token, minValiditySec, { replace? })` 를 더했다
|
|
63
|
+
(실시간 클라이언트가 쓰는 내부용 공개 메서드).
|
|
64
|
+
|
|
65
|
+
### Changed — 멤버 폐기와 토큰 만료로 닫힐 때의 알림
|
|
66
|
+
|
|
67
|
+
- 서버가 멤버를 정지, 삭제하거나 세션을 모두 끊으면(강제 로그아웃, `signOut()` — 모든 기기 로그아웃, 비밀번호 변경과
|
|
68
|
+
재설정) 그 멤버의 실시간 연결을 바로 닫는다(close 4403 `member_revoked`). 닫기 직전의 서버 안내(`MEMBER_REVOKED`,
|
|
69
|
+
`TOKEN_EXPIRED`)를 따로 `onError` 로 보내지 않고, 이어지는 close 의 `RealtimeConnectionError` 메시지로 한 번만 알린다.
|
|
70
|
+
- `cb.realtime`: 토큰 만료(4401)로 닫혔는데 SDK 세션이 살아 있으면 새 토큰으로 다시 붙는다. 멤버 폐기(4403)는 다시 붙지
|
|
71
|
+
않는다.
|
|
72
|
+
- `cb.database` 실시간: 멤버 폐기(4403)와 갱신할 수 없는 토큰의 만료(4401)는 재연결하지 않고 `onRealtimeError` 로
|
|
73
|
+
`RealtimeConnectionError`(`code: 'member_revoked'` / `'token_expired'`)를 알린다. 예전에는 같은 토큰으로 다섯 번 재연결한
|
|
74
|
+
뒤 "Max retries exceeded" 로 끝났다. 그 밖의 끊김은 예전처럼 다시 붙는다.
|
|
75
|
+
|
|
76
|
+
### Added — `ctx.cbAdmin.appMembers.suspend()` / `unsuspend()` (management_scope `app_member:suspend`)
|
|
77
|
+
|
|
78
|
+
- service_role 함수가 회원을 정지하고 푼다. 신고 처리, 스팸이나 결제 사기 자동 제재처럼 되돌릴 수 있는 차단을 함수로 만들 때
|
|
79
|
+
쓴다. 함수의 `management_scopes` 에 `app_member:suspend` 를 넣어야 하고, 위임하는 사람에게는 콘솔의 `app_member:write` 가
|
|
80
|
+
필요하다. 삭제(`app_member:write`)와는 서로 함의하지 않는다.
|
|
81
|
+
- `suspend(appId, memberId, { reason, until? })`: `reason`(1~500자, 정지된 회원에게 보이는 문구)은 필수이고, `until`
|
|
82
|
+
(`Date` 또는 ISO 문자열)을 생략하면 `unsuspend` 할 때까지 영구 정지다. 콘솔의 정지와 같이 그 회원의 세션을 모두 끊고
|
|
83
|
+
붙어 있는 실시간 연결을 닫는다(`member_revoked`).
|
|
84
|
+
- `unsuspend(appId, memberId)`: 정지를 푼다. 끊긴 세션은 되살아나지 않는다(회원은 다시 로그인한다).
|
|
85
|
+
- 함수 런타임은 배포 때 최신 SDK 를 설치하므로, 이 판이 나온 뒤 함수를 다시 배포하면 쓸 수 있다.
|
|
86
|
+
|
|
87
|
+
## [6.11.0] - 2026-11-11
|
|
88
|
+
|
|
89
|
+
### Security — CLI 브라우저 로그인은 터미널의 사용자 코드로 승인한다
|
|
90
|
+
|
|
91
|
+
- `connectbase login` 이 터미널에 사용자 코드(XXXX-XXXX)를 보여 주고, 브라우저에서는 그 코드를 직접 입력해 승인한다
|
|
92
|
+
(RFC 8628 방식). 예전에는 승인 링크가 곧 폴링 자격이어서, 남이 만든 링크를 승인하면 그 사람이 내 계정의 Secret Key 를
|
|
93
|
+
받았다. 승인 화면은 요청한 기기, OS, IP, 시각을 보여 주고, 거부하면 CLI 가 바로 끝난다.
|
|
94
|
+
- 구버전 CLI 는 서버가 `426 cli_upgrade_required` 로 거절한다. 이 버전 이상으로 올려야 로그인할 수 있다.
|
|
95
|
+
|
|
96
|
+
### Changed — 로그인한 적 없는 방문자의 세션 복구가 브라우저 콘솔 오류를 남기지 않는다
|
|
97
|
+
|
|
98
|
+
- 쿠키만으로 세션을 복구할 때 `/v1/auth/re-issue?quiet=1` 로 묻고, 세션이 없으면 서버가 401 대신 204 로 답한다. 앱이 받는
|
|
99
|
+
결과(세션 없음, `onTokenExpired`)는 그대로이고, 매 페이지 「Failed to load resource 401」 오류(Lighthouse 콘솔 오류)만 사라진다.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
### Security — 소셜 로그인 콜백은 이 브라우저에서 시작한 로그인만 받는다
|
|
103
|
+
|
|
104
|
+
- `cb.oauth.signIn()` / `signUp()` 이 일회용 `state` 를 이 브라우저(localStorage)에 남기고 서버로 보낸다. 콜백의 토큰이나
|
|
105
|
+
`code` 는 그 값이 돌아올 때만 받는다. 확인되지 않으면 토큰을 쓰지 않고 `getCallbackResult()` 가
|
|
106
|
+
`{ error: 'oauth_state_mismatch' }` 를, `exchangeCodeFromCallback()` 이 같은 코드의 오류를 던진다. 남이 보낸 콜백
|
|
107
|
+
링크로 다른 계정에 로그인되던 문제(로그인 CSRF)를 막는다. 기존 세션은 끊지 않는다.
|
|
108
|
+
- 앱이 넘긴 `state` 는 서버로 보내지 않고 `result.state` 로 그대로 돌려준다.
|
|
109
|
+
- URL 의 토큰은 `new ConnectBase()` 때 `history.replaceState` 로 바로 지운다(`autoRestoreSession` 과 무관). `location.search`
|
|
110
|
+
로 토큰을 직접 읽던 코드는 `getCallbackResult()` 결과를 써야 한다.
|
|
111
|
+
- 인가 URL 을 직접 만드는 앱을 위한 `cb.oauth.createCallbackState(appState?)` 추가. 그 값을 `state` 쿼리로 넘긴다.
|
|
112
|
+
- 콜백 주소는 로그인을 시작한 페이지와 같은 출처여야 한다(다르면 시작할 때 콘솔 경고).
|
|
113
|
+
- 팝업: 우리가 연 팝업(`event.source`)에서 우리가 아는 출처(`event.origin`)로 온 메시지만 받는다. 서버 제공 콜백을 쓰면
|
|
114
|
+
`opener_origin` 을 보내 1회용 `code` 를 받아 교환한다. 앱이 지정한 팝업 콜백 페이지는 같은 출처 창에만 결과를 보낸다.
|
|
115
|
+
서버의 `popup_opener_not_allowed` 는 해결 방법을 담은 `ApiError`(같은 `code`)로 던진다.
|
|
116
|
+
- 분석(`page_url`, `referrer`)과 에러 리포트(`url`, `source`, `message`, `stack`)에서 토큰, `code` 같은 자격증명을 지운다.
|
|
117
|
+
|
|
118
|
+
### Security — 그 밖
|
|
119
|
+
|
|
120
|
+
- 게임 상태 델타의 `__proto__`, `constructor`, `prototype` 경로를 버리고 자기 속성으로만 따라간다(프로토타입 오염).
|
|
121
|
+
- 브라우저에서 `secretKey`(또는 `cb_sk_` 를 publicKey 로)를 쓰면 경고한다.
|
|
122
|
+
- CLI: 서버 주소는 `--base-url` > `CONNECTBASE_BASE_URL` > `.connectbaserc` 순이다. 저장소의 `.connectbaserc` 가 공식 서버가
|
|
123
|
+
아닌 주소를 정하면 터미널에서는 확인을 묻고 비대화형에서는 중단한다(`--allow-repo-base-url` 로 허용). `.connectbaserc` 의
|
|
124
|
+
`deployDir` 이 프로젝트 밖이면 배포하지 않는다. 브라우저 열기는 셸을 거치지 않는다. `.connectbaserc`, `.mcp.json` 은
|
|
125
|
+
0600 으로 쓴다.
|
|
126
|
+
|
|
127
|
+
### Fixed — 큰 웹 배포가 「요청 본문이 상한을 초과했습니다」로 실패하던 문제
|
|
128
|
+
|
|
129
|
+
서버는 배포 요청 하나를 128 MiB 로 자릅니다. 변경 파일이 많으면(예: 영상 120개, 148MB) 한 요청에 다 실어 실패했고, 나눠
|
|
130
|
+
올리면 release 가 여러 개로 쪼개져 중간 판이 잠깐 사이트에 나갔습니다. 이제 요청 하나가 약 96 MiB 를 넘으면 바이너리를 큰
|
|
131
|
+
것부터 Object Storage 로 직접 올려(presign, 이미 있던 경로) 요청 하나, release 하나로 반영합니다. 텍스트만으로 넘치면 이유를
|
|
132
|
+
알리고 멈춥니다.
|
|
133
|
+
|
|
6
134
|
## [6.10.0] - 2026-10-30
|
|
7
135
|
|
|
8
136
|
### Added — 실시간 메시지의 `fromConn`(보낸 연결의 서버 ID)
|
package/README.md
CHANGED
|
@@ -907,6 +907,8 @@ await cb.storage.deletePageMeta('web-storage-id', '/products/123')
|
|
|
907
907
|
|
|
908
908
|
문서를 등록하고 BM25 풀텍스트 검색으로 관련 청크를 찾는 RAG 인프라. 한국어는 nori 형태소 분석기 적용. AI 채팅에 컨텍스트로 연결하려면 `cb.ai.chatStream({ knowledgeBaseId })` 를 사용한다.
|
|
909
909
|
|
|
910
|
+
> 문서 추가, 수정, 삭제는 로그인한 회원 토큰(본인 자료)이나 `secretKey`(서버)가 필요합니다. Public Key 만으로는 401 `KB_WRITE_CREDENTIAL_REQUIRED` 입니다.
|
|
911
|
+
|
|
910
912
|
```typescript
|
|
911
913
|
// 텍스트 문서 추가 (즉시 처리)
|
|
912
914
|
const doc = await cb.knowledge.addDocument('kb-id', {
|
|
@@ -998,6 +1000,21 @@ await subscription.unsubscribe()
|
|
|
998
1000
|
await cb.realtime.disconnect()
|
|
999
1001
|
```
|
|
1000
1002
|
|
|
1003
|
+
#### 로그인 멤버 연결의 토큰 갱신과 멤버 폐기 (Unreleased)
|
|
1004
|
+
|
|
1005
|
+
`cb.realtime.connect({ accessToken })` 과 `cb.database.connectRealtime({ accessToken })` 에 넘긴 토큰이 SDK 가 로그인으로
|
|
1006
|
+
들고 있는 멤버 세션(같은 앱, 같은 멤버)의 것이면, SDK 가 만료 전에 HTTP 요청과 같은 refresh 로 새 토큰을 받아 **연결을 끊지
|
|
1007
|
+
않고** 바꿉니다(서버와 `token_refresh=1` 로 협상, `auth_refresh`). 재연결도 새 토큰을 씁니다. 앱이 할 일은 없습니다.
|
|
1008
|
+
|
|
1009
|
+
| 상황 | 결과 (`onError` / `onRealtimeError`) |
|
|
1010
|
+
|---|---|
|
|
1011
|
+
| 갱신할 수 없는 토큰(다른 곳에서 받은 토큰, 위임 토큰)이 만료되고 24시간 지남 | `RealtimeConnectionError`, `code: 'token_expired'`(close 4401). 재연결하지 않음 — 새 토큰으로 다시 연결 |
|
|
1012
|
+
| SDK 세션이 끝나 갱신 실패(로그아웃, refresh 거부) | 만료 5분 뒤 같은 `token_expired` |
|
|
1013
|
+
| 멤버 정지, 삭제, 세션 전체 종료(강제 로그아웃, `signOut()`, 비밀번호 변경) | `code: 'member_revoked'`(close 4403), 메시지는 서버 안내. 재연결하지 않음 — 다시 로그인 |
|
|
1014
|
+
|
|
1015
|
+
로그아웃 버튼에서는 `cb.realtime.disconnect()` (DB 실시간은 `cb.database.disconnectRealtime()`)를 `cb.auth.signOut()` 보다
|
|
1016
|
+
먼저 부르면 `member_revoked` 알림 없이 끝납니다 — `signOut()` 은 모든 기기의 세션을 끊어 서버가 그 멤버의 연결을 닫습니다.
|
|
1017
|
+
|
|
1001
1018
|
#### Who sent it: `fromMember` (v6.7.0)
|
|
1002
1019
|
|
|
1003
1020
|
`message.from` 은 보낸 연결이 접속할 때 스스로 정해 보내는 `client_id` 라 누구나 다른 값을 흉내 낼 수 있습니다. 서버가 검증한
|
|
@@ -1658,6 +1675,8 @@ await cb.support.reportIssue({
|
|
|
1658
1675
|
| 스코프 | 열리는 메서드 |
|
|
1659
1676
|
|---|---|
|
|
1660
1677
|
| `app_member:read` | `appMembers.list` / `appMembers.get` — 멤버 목록·상세 (**이메일 포함**) |
|
|
1678
|
+
| `app_member:write` | `appMembers.remove` — 멤버 삭제 (되돌릴 수 없음) |
|
|
1679
|
+
| `app_member:suspend` | `appMembers.suspend(appId, memberId, { reason, until? })` / `appMembers.unsuspend` — 정지와 해제 (세션 종료, 실시간 연결 끊기 포함. `until` 생략 시 영구) |
|
|
1661
1680
|
| `role:read` | `roles.list` / `roles.get` |
|
|
1662
1681
|
| `role:manage` | `roles.create` / `roles.update` / `roles.assign` / `roles.delete` |
|
|
1663
1682
|
| `payment:read` | `payment.list` — 결제 내역 |
|
|
@@ -1689,7 +1708,7 @@ export async function handler(payload, ctx) {
|
|
|
1689
1708
|
|
|
1690
1709
|
`appMembers.list` 의 `email` 은 `app_members.email` 컬럼이 우선이고, 비어 있으면 `EMAIL` identity 의 `provider_uid` 로 fallback 한다. 소셜 제공자가 이메일을 주지 않은 회원은 빈 문자열이다. `total_count` 는 `search` 필터를 반영하므로 페이지네이션에 그대로 쓸 수 있다.
|
|
1691
1710
|
|
|
1692
|
-
**멤버가 자기 정보를 볼 때는 `cb.auth.getMe()` 를 쓴다.** `appMembers
|
|
1711
|
+
**멤버가 자기 정보를 볼 때는 `cb.auth.getMe()` 를 쓴다.** `appMembers.list/get` 은 운영자 방향 조회이고, 삭제(`app_member:write`)와 정지(`app_member:suspend`)는 각자의 스코프가 따로 필요하다 — `app_member:read` 로는 열리지 않는다. 생성과 수정은 콘솔 전용이다.
|
|
1693
1712
|
|
|
1694
1713
|
```typescript
|
|
1695
1714
|
// 역할(RBAC) 관리 — management_scopes: ["role:read", "role:manage"]
|