connectbase-client 6.0.0 → 6.2.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 +166 -0
- package/README.md +119 -7
- package/dist/cli.js +62 -11
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +969 -22
- package/dist/index.d.ts +969 -22
- package/dist/index.js +679 -7
- package/dist/index.mjs +678 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,172 @@
|
|
|
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.2.0] - 2026-09-06
|
|
7
|
+
|
|
8
|
+
### Added — 웹 스토리지 관리 API 에 Secret Key 전송
|
|
9
|
+
|
|
10
|
+
`deploy`, `promote`, `releases` 와 `init` 의 스토리지 목록/생성이 이제 Secret Key(`cb_sk_*`)를
|
|
11
|
+
함께 보냅니다. `.connectbaserc` 의 `secretKey` 또는 `CONNECTBASE_SECRET_KEY` 환경변수에서
|
|
12
|
+
자동으로 읽습니다. **`init` 을 이미 실행했다면 할 일이 없습니다.**
|
|
13
|
+
|
|
14
|
+
**왜 필요한가.** Public Key(`cb_pk_*`)는 비밀이 아닙니다. 브라우저 번들에 실려 배포되는 공개
|
|
15
|
+
식별자이고, 실제로 운영 중인 사이트의 JS 에서 그대로 추출됩니다. 그런데 이 값 하나로 배포와
|
|
16
|
+
production 승격이 가능했습니다 — 공개된 사이트에서 키를 뽑은 제3자가 그 사이트를 통째로
|
|
17
|
+
교체할 수 있었다는 뜻입니다. Public Key 는 "어느 앱인가" 를 말할 뿐 "이 앱을 다룰 자격이
|
|
18
|
+
있는가" 를 증명하지 못하므로, 쓰기 동작의 자격증명이 될 수 없습니다.
|
|
19
|
+
|
|
20
|
+
**서버는 곧 이를 요구합니다.** 현재는 기존 배포 파이프라인이 끊기지 않도록 유예 중이고,
|
|
21
|
+
이 버전이 충분히 퍼진 뒤 강제로 전환합니다. 그때부터 Secret Key 없는 호출은 401
|
|
22
|
+
`SECRET_KEY_REQUIRED` 로 거부됩니다.
|
|
23
|
+
|
|
24
|
+
**이 버전은 아무것도 깨뜨리지 않습니다.** Secret Key 가 없으면 경고만 하고 그대로 진행합니다.
|
|
25
|
+
CLI 가 서버보다 먼저 조이면, 우리가 생성해 드리는 GitHub Actions 워크플로처럼 버전을 고정하지
|
|
26
|
+
않는(`npx --yes connectbase deploy`) 환경이 서버 변경 전에 깨지기 때문입니다.
|
|
27
|
+
|
|
28
|
+
**지금 해두실 것.** CI 를 쓰신다면 저장소 시크릿에 `CONNECTBASE_SECRET_KEY` 를 추가하고
|
|
29
|
+
워크플로 env 에 넣으세요. 워크플로 파일에 값을 직접 적으면 저장소에 커밋됩니다.
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
env:
|
|
33
|
+
CONNECTBASE_PUBLIC_KEY: ${{ secrets.CONNECTBASE_PUBLIC_KEY }}
|
|
34
|
+
CONNECTBASE_SECRET_KEY: ${{ secrets.CONNECTBASE_SECRET_KEY }}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`page-metas` 계열 API 는 **변경 없습니다** — 브라우저 SDK 의 정규 경로라 그대로 둡니다.
|
|
38
|
+
|
|
39
|
+
### Added — `X-User-Secret-Key` 전용 헤더
|
|
40
|
+
|
|
41
|
+
Secret Key 를 `Authorization` 대신 전용 헤더로도 보냅니다.
|
|
42
|
+
|
|
43
|
+
`Authorization` 자리는 AppMember 세션 토큰이 먼저 차지합니다. 종전 SDK 는 그 헤더가 이미
|
|
44
|
+
있으면 Secret Key 를 붙이지 않았기 때문에, **로그인 상태 브라우저에서는 `secretKey` 를 설정해도
|
|
45
|
+
전송되지 않았습니다.** 두 자격증명은 역할이 다릅니다 — 멤버 토큰은 "누가 이 앱의 회원인가",
|
|
46
|
+
Secret Key 는 "이 호출자가 앱을 관리할 자격이 있는가". 한 헤더를 두고 다투게 두면 관리 동작이
|
|
47
|
+
로그인 상태에서만 조용히 실패합니다.
|
|
48
|
+
|
|
49
|
+
서버는 전용 헤더를 `Authorization` 보다 우선해 읽습니다. 기존 `Authorization` 경로도 그대로
|
|
50
|
+
보내므로 구버전 서버와 호환됩니다.
|
|
51
|
+
|
|
52
|
+
> Secret Key 는 서버사이드 자격증명입니다. 브라우저 번들에 넣지 마세요.
|
|
53
|
+
|
|
54
|
+
### Fixed — 대용량 파일 배포 시 원인 불명 500
|
|
55
|
+
|
|
56
|
+
파일당 저장 한도(약 6 MiB, 바이너리는 base64 로 약 1.33배)를 넘으면 서버가 원시 DB 오류를
|
|
57
|
+
500 으로 뭉개 반환했습니다. 이제 **413** 과 함께 초과 파일 목록, 원본 크기 추정치, presign
|
|
58
|
+
경로 안내가 응답에 담깁니다. CLI 는 종전대로 대용량 바이너리를 자동으로 오프로드합니다.
|
|
59
|
+
|
|
60
|
+
## [6.1.0] - 2026-09-01
|
|
61
|
+
|
|
62
|
+
### Added — `cb.organizations.*`: 조직/팀(워크스페이스)
|
|
63
|
+
|
|
64
|
+
앱의 **엔드유저가 만드는 조직**을 다루는 모듈입니다. Connect Base 콘솔의 협업자 RBAC
|
|
65
|
+
(`cb.roles.*`)와는 완전히 별개 시스템입니다.
|
|
66
|
+
|
|
67
|
+
| 메서드 | 설명 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `create` / `listMine` / `get` / `update` / `delete` | 조직 CRUD (`slug` 는 생성 후 변경 불가) |
|
|
70
|
+
| `listMembers` / `updateMemberRole` / `removeMember` | 멤버 관리 (본인 ID 로 `removeMember` 하면 탈퇴) |
|
|
71
|
+
| `createInvitation` / `listInvitations` / `revokeInvitation` / `acceptInvitation` | 초대 |
|
|
72
|
+
| `switchTo` | 조직 전환 — 조직 컨텍스트가 실린 새 액세스 토큰을 받아 SDK 에 자동 적용 |
|
|
73
|
+
|
|
74
|
+
모든 조직 API 는 **로그인한 회원 토큰**을 요구합니다. 퍼블릭 키만으로 호출하면 401 입니다 —
|
|
75
|
+
퍼블릭 키는 설계상 클라이언트 번들에 노출되는 값이라, 그것만으로 목록을 열면 앱의 조직 구조가
|
|
76
|
+
통째로 덤프되기 때문입니다.
|
|
77
|
+
|
|
78
|
+
> #### 조직 컨텍스트는 10분마다 갱신해야 합니다
|
|
79
|
+
>
|
|
80
|
+
> `switchTo()` 가 심는 RLS 조직 컨텍스트(`auth.org_id` / `auth.org_role`)의 수명은 액세스
|
|
81
|
+
> 토큰(1시간)과 **독립적인 10분**입니다. 만료되어도 토큰 자체는 유효해 401 이 나지 않고,
|
|
82
|
+
> 조직 규칙만 fail-closed 로 거부되어 **"멀쩡하던 조회가 갑자기 전부 막히는"** 증상으로만
|
|
83
|
+
> 드러납니다. 반환값의 `org_context_expires_in`(초) 보다 먼저 다시 호출하세요.
|
|
84
|
+
> 토큰 회전(refresh)이 일어나도 조직 컨텍스트는 의도적으로 사라지므로 다시 호출해야 합니다.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
88
|
+
|
|
89
|
+
async function activate(orgId: string) {
|
|
90
|
+
const ctx = await cb.organizations.switchTo(orgId)
|
|
91
|
+
clearTimeout(timer)
|
|
92
|
+
timer = setTimeout(
|
|
93
|
+
() => activate(orgId),
|
|
94
|
+
Math.max(ctx.org_context_expires_in - 60, 30) * 1000,
|
|
95
|
+
)
|
|
96
|
+
return ctx
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Changed — `subscription.deleteBillingKey()` 가 삭제 결과를 반환합니다
|
|
101
|
+
|
|
102
|
+
반환 타입이 `Promise<void>` 에서 `Promise<DeleteBillingKeyResponse>` 로 바뀌었습니다.
|
|
103
|
+
서버는 이전부터 이 본문을 보내고 있었지만 타입에 노출되지 않아, **"PG 등록이 남아 있다"는
|
|
104
|
+
경고를 SDK 사용자가 볼 수 없었습니다.**
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const result = await cb.subscription.deleteBillingKey(billingKeyId)
|
|
108
|
+
if (!result.provider_revoked) {
|
|
109
|
+
// Connect Base 기록만 지워졌고 PG 등록은 그대로 남아 있다
|
|
110
|
+
alert(result.provider_revoke_note)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| 프로바이더 | PG 해지 | 동작 |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `toss` | O | 토스 빌링키 삭제 API 호출. PG 삭제가 실패하면 DB 행도 지우지 않고 에러 |
|
|
117
|
+
| `payapp` / `paypal` / `paddle` / `stripe` | X | "결제수단 삭제" API 가 없어 기록만 삭제. `provider_revoked: false` + `provider_revoke_note` |
|
|
118
|
+
|
|
119
|
+
반환값을 쓰지 않던 기존 호출은 그대로 동작합니다 (하위호환).
|
|
120
|
+
|
|
121
|
+
### Added — `DeleteBillingKeyResponse` 타입
|
|
122
|
+
|
|
123
|
+
`message` / `provider` / `provider_revoked` / `provider_revoke_note?`.
|
|
124
|
+
|
|
125
|
+
### Added — 퍼블릭 키 스코프와 무중단 회전
|
|
126
|
+
|
|
127
|
+
- `cb.publicKey.createPublicKey()` 가 `scopes` 를 받습니다. **비우면 전권**이고(기존 키가 전부
|
|
128
|
+
그렇습니다), 어휘는 새로 만든 것이 아니라 콘솔 RBAC 권한 이름과 service_role
|
|
129
|
+
`management_scopes` 의 합집합입니다.
|
|
130
|
+
- `PublicKeyItem` / `CreatePublicKeyResponse` / `UpdatePublicKeyResponse` 에 `scopes` 가 실립니다.
|
|
131
|
+
`PublicKeyItem` 에는 발급 출처 감사 필드(`created_by_kind`, `created_by_user_id`,
|
|
132
|
+
`created_ip_masked`, `created_user_agent`)도 추가됐습니다.
|
|
133
|
+
- **`cb.publicKey.rotatePublicKey(appId, keyId, { grace_period_hours?, name? })`** 신설 —
|
|
134
|
+
새 키를 발급하고 옛 키는 유예 기간(기본 24시간, `0` = 즉시, 상한 720시간) 뒤 만료됩니다.
|
|
135
|
+
옛 키의 스코프와 `payment_mode` 는 새 키가 물려받습니다. 반환값의 `new_key.key` 는 이때만
|
|
136
|
+
볼 수 있고, `previous_key_expires_at` 전에 클라이언트 배포를 끝내야 합니다.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const rotated = await cb.publicKey.rotatePublicKey('app-id', 'key-id', { grace_period_hours: 72 })
|
|
140
|
+
console.log(rotated.new_key.key, rotated.previous_key_expires_at)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Added — `HttpClient.setAccessToken()`
|
|
144
|
+
|
|
145
|
+
refresh token 을 건드리지 않고 액세스 토큰만 교체합니다. 조직 컨텍스트 발급처럼 **새 액세스
|
|
146
|
+
토큰만 돌려주는** 엔드포인트를 위한 것으로, `cb.organizations.switchTo()` 가 내부에서 씁니다.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## [6.0.1] - 2026-08-22
|
|
151
|
+
|
|
152
|
+
### Fixed — 로그인 세션이 없을 때 `/v1/auth/re-issue` 401 이 반복해서 나가던 문제
|
|
153
|
+
|
|
154
|
+
쿠키 세션이 없는 브라우저(제3자 쿠키 차단, 쿠키 삭제, 만료 등)에서 인증이 필요한 API 를
|
|
155
|
+
호출하면 SDK 가 매번 "쿠키로 세션을 복구해 보자"며 `/v1/auth/re-issue` 를 한 번씩 더
|
|
156
|
+
보냈고, 그 요청이 401 로 떨어졌습니다. 앱이 폴링을 하거나 화면/훅마다 클라이언트를 새로
|
|
157
|
+
만드는 구조면 이 401 이 수 초 간격으로 계속 쌓입니다 (실측: 5~6초 간격으로 13분 이상 연속,
|
|
158
|
+
platform-issue `01a0237f`).
|
|
159
|
+
|
|
160
|
+
동시 refresh 를 하나로 합치는 처리(single-flight)는 이전부터 있었지만 **클라이언트 인스턴스
|
|
161
|
+
하나가 살아 있는 동안만** 유효해서, 인스턴스가 새로 만들어지면 백오프까지 함께 초기화되는
|
|
162
|
+
것이 원인이었습니다.
|
|
163
|
+
|
|
164
|
+
이제 쿠키만으로 시도한 재발급이 401/403 을 받으면 "지금 이 브라우저에는 쓸 수 있는 refresh
|
|
165
|
+
쿠키가 없다"를 60초 동안 `localStorage` 에 기록하고, 그동안은 **인스턴스가 달라도** 재발급
|
|
166
|
+
요청을 보내지 않습니다. 로그인(`setTokens`)이나 성공한 재발급은 이 기록을 즉시 지우므로
|
|
167
|
+
세션이 생기는 즉시 정상 동작합니다. 메모리에 refresh token 을 들고 있는 호출은 자격이
|
|
168
|
+
다르므로 영향받지 않습니다.
|
|
169
|
+
|
|
170
|
+
동작 변화는 "불필요한 401 요청이 사라지는 것"뿐입니다 — 세션 복구 성공 조건은 그대로입니다.
|
|
171
|
+
|
|
6
172
|
## [6.0.0] - 2026-08-20
|
|
7
173
|
|
|
8
174
|
### BREAKING — `realtime.isConnected()` 가 SSE 폴백에서 더 이상 true 를 반환하지 않습니다
|
package/README.md
CHANGED
|
@@ -27,13 +27,19 @@ Connect Base provides **two types** of Keys. Use the right key for your use case
|
|
|
27
27
|
|---------|----------|---------|
|
|
28
28
|
| Frontend SDK (`new ConnectBase()`) | **Public Key** (`cb_pk_`) | Web/app: DB queries, auth, file uploads |
|
|
29
29
|
| `.env` file (`VITE_CONNECTBASE_PUBLIC_KEY`) | **Public Key** (`cb_pk_`) | React, Vue, etc. |
|
|
30
|
-
| CLI deploy (`.connectbaserc`) | **Public Key** (`cb_pk_`) | `npx connectbase deploy` |
|
|
30
|
+
| CLI deploy (`.connectbaserc`) | **Public Key + Secret Key** (`cb_pk_` + `cb_sk_`) | `npx connectbase deploy` |
|
|
31
31
|
| MCP server (AI tools) | **Secret Key** (`cb_sk_`) | Claude, Cursor, Windsurf |
|
|
32
32
|
| Server-side admin tasks | **Secret Key** (`cb_sk_`) | Backend full data access |
|
|
33
33
|
|
|
34
34
|
> ⚠️ **MCP server rejects Public Keys** — you must use a Secret Key (`cb_sk_`).
|
|
35
35
|
>
|
|
36
36
|
> ⚠️ **Never use Secret Keys in frontend code** — RLS is bypassed, exposing all data.
|
|
37
|
+
>
|
|
38
|
+
> 🔐 **Web storage deploys need both keys.** A Public Key ships inside your browser bundle, so
|
|
39
|
+
> anyone can read it out of a deployed site's JS — it cannot prove you may replace that site.
|
|
40
|
+
> The web storage list/create/deploy/promote routes therefore require
|
|
41
|
+
> `Authorization: Bearer cb_sk_*` **in addition to** `X-Public-Key`; without it they return
|
|
42
|
+
> **401 `SECRET_KEY_REQUIRED`**. The CLI sends it for you — see [CLI](#cli).
|
|
37
43
|
|
|
38
44
|
Create Keys in the Console under **Settings > API tab**. Choose Public or Secret type when creating. The full key is shown **only once** at creation time.
|
|
39
45
|
|
|
@@ -113,6 +119,7 @@ try {
|
|
|
113
119
|
- **Push Notifications**: Cross-platform push notification support
|
|
114
120
|
- **WebRTC**: Real-time audio/video communication
|
|
115
121
|
- **Payments**: Subscription and one-time payment support
|
|
122
|
+
- **Organizations**: End-user workspaces/teams with invitations and an RLS organization context (`cb.organizations.*`)
|
|
116
123
|
- **AI Streaming**: Real-time AI text generation via WebSocket (multi-provider: Gemini, OpenAI, Claude, Ollama, LM Studio, OpenAI-compatible)
|
|
117
124
|
- **Knowledge Base (RAG)**: Document indexing + BM25 search with nori 한국어 형태소. PDF / DOCX / text file upload via `addDocumentFromFile`
|
|
118
125
|
- **Endpoint**: Call your own GPU models on your own PC through one `cb_pk_*` key — ConnectBase forwards the payload as-is (dumb pipe)
|
|
@@ -124,6 +131,17 @@ try {
|
|
|
124
131
|
|
|
125
132
|
Deploy your web application to Connect Base Web Storage with a single command.
|
|
126
133
|
|
|
134
|
+
> 🔐 **Deploy commands require a Secret Key (`cb_sk_*`).** The web storage deploy, promote,
|
|
135
|
+
> create and list routes require `Authorization: Bearer cb_sk_*` on top of `X-Public-Key`,
|
|
136
|
+
> because a Public Key is a public identifier that ships in your browser bundle and cannot
|
|
137
|
+
> prove deploy permission. The CLI reads `secretKey` from `.connectbaserc` or the
|
|
138
|
+
> `CONNECTBASE_SECRET_KEY` environment variable and sends it automatically — **just update to
|
|
139
|
+
> the latest version** (`npm i -g connectbase-client@latest`). Older CLIs fail with
|
|
140
|
+
> **401 `SECRET_KEY_REQUIRED`**.
|
|
141
|
+
>
|
|
142
|
+
> **Never commit a Secret Key.** `init` adds `.connectbaserc` to `.gitignore`; in CI, inject it
|
|
143
|
+
> from a repository secret instead.
|
|
144
|
+
|
|
127
145
|
### Quick Start
|
|
128
146
|
|
|
129
147
|
```bash
|
|
@@ -135,9 +153,10 @@ npm run deploy
|
|
|
135
153
|
```
|
|
136
154
|
|
|
137
155
|
The `init` command will:
|
|
138
|
-
- Ask for your
|
|
156
|
+
- Ask for your Secret Key (`cb_sk_`) — or take it from `--secret-key` / `CONNECTBASE_SECRET_KEY`
|
|
157
|
+
- Issue a Public Key for the app
|
|
139
158
|
- List existing web storages or create a new one automatically
|
|
140
|
-
- Create a `.connectbaserc` config file
|
|
159
|
+
- Create a `.connectbaserc` config file (holds both keys)
|
|
141
160
|
- Add `.connectbaserc` to `.gitignore`
|
|
142
161
|
- Add a `deploy` script to `package.json` (includes `build` if available)
|
|
143
162
|
|
|
@@ -154,7 +173,14 @@ The `init` command will:
|
|
|
154
173
|
If you prefer not to use `init`, you can pass options directly:
|
|
155
174
|
|
|
156
175
|
```bash
|
|
157
|
-
npx connectbase deploy ./dist -s <storage-id> -k <public-key>
|
|
176
|
+
npx connectbase deploy ./dist -s <storage-id> -k <public-key> --secret-key <secret-key>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The Secret Key can also come from `CONNECTBASE_SECRET_KEY` — preferred in CI, so it never
|
|
180
|
+
appears in shell history or CI logs:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
CONNECTBASE_SECRET_KEY=cb_sk_... npx connectbase deploy ./dist -s <storage-id> -k <public-key>
|
|
158
184
|
```
|
|
159
185
|
|
|
160
186
|
### Options
|
|
@@ -162,7 +188,8 @@ npx connectbase deploy ./dist -s <storage-id> -k <public-key>
|
|
|
162
188
|
| Option | Alias | Description |
|
|
163
189
|
|--------|-------|-------------|
|
|
164
190
|
| `--storage <id>` | `-s` | Storage ID |
|
|
165
|
-
| `--public-key <key>` | `-k` |
|
|
191
|
+
| `--public-key <key>` | `-k` | Public Key (`cb_pk_`) — identifies the app |
|
|
192
|
+
| `--secret-key <key>` | | Secret Key (`cb_sk_`) — required by `deploy` / `promote` / storage list & create. Prefer `CONNECTBASE_SECRET_KEY` in CI |
|
|
166
193
|
| `--base-url <url>` | `-u` | Custom server URL |
|
|
167
194
|
| `--timeout <sec>` | `-t` | Tunnel request timeout in seconds (tunnel only) |
|
|
168
195
|
| `--max-body <MB>` | | Tunnel max body size in MB (tunnel only) |
|
|
@@ -228,20 +255,37 @@ The `init` command creates `.connectbaserc` automatically. You can also create i
|
|
|
228
255
|
|
|
229
256
|
```json
|
|
230
257
|
{
|
|
231
|
-
"publicKey": "
|
|
258
|
+
"publicKey": "cb_pk_your-public-key",
|
|
259
|
+
"secretKey": "cb_sk_your-secret-key",
|
|
232
260
|
"storageId": "your-storage-id",
|
|
233
261
|
"deployDir": "./dist"
|
|
234
262
|
}
|
|
235
263
|
```
|
|
236
264
|
|
|
265
|
+
> ⚠️ `.connectbaserc` now holds a Secret Key — keep it in `.gitignore` (`init` adds it) and never
|
|
266
|
+
> commit it. On a shared machine or in CI, drop `secretKey` from the file and pass
|
|
267
|
+
> `CONNECTBASE_SECRET_KEY` instead.
|
|
268
|
+
|
|
237
269
|
### Environment Variables
|
|
238
270
|
|
|
239
271
|
```bash
|
|
240
|
-
export CONNECTBASE_PUBLIC_KEY=
|
|
272
|
+
export CONNECTBASE_PUBLIC_KEY=cb_pk_your-public-key
|
|
273
|
+
export CONNECTBASE_SECRET_KEY=cb_sk_your-secret-key # required by deploy / promote
|
|
241
274
|
export CONNECTBASE_STORAGE_ID=your-storage-id
|
|
242
275
|
npx connectbase deploy ./dist
|
|
243
276
|
```
|
|
244
277
|
|
|
278
|
+
In CI, read the Secret Key from a repository secret:
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
# GitHub Actions
|
|
282
|
+
- run: npx connectbase deploy ./dist
|
|
283
|
+
env:
|
|
284
|
+
CONNECTBASE_PUBLIC_KEY: ${{ vars.CONNECTBASE_PUBLIC_KEY }}
|
|
285
|
+
CONNECTBASE_SECRET_KEY: ${{ secrets.CONNECTBASE_SECRET_KEY }}
|
|
286
|
+
CONNECTBASE_STORAGE_ID: ${{ vars.CONNECTBASE_STORAGE_ID }}
|
|
287
|
+
```
|
|
288
|
+
|
|
245
289
|
### Requirements
|
|
246
290
|
|
|
247
291
|
- `index.html` must exist in the root of the deploy directory
|
|
@@ -1330,6 +1374,20 @@ call.disconnect() // voice 는 그대로 유지됩니다
|
|
|
1330
1374
|
|
|
1331
1375
|
### Payments & Subscriptions
|
|
1332
1376
|
|
|
1377
|
+
> **빌링키 API 6종(발급/확인/목록/상세/수정/삭제)은 로그인한 회원 토큰이 필요합니다.**
|
|
1378
|
+
> `X-Public-Key` 단독 호출은 `401` 입니다. 로그인 없이 빌링키를 만들던 게스트 체크아웃 흐름은
|
|
1379
|
+
> 회원 로그인 후 호출로 바꿔야 합니다. 서버에서는 `cb_sk_*` 또는 `management_scopes` 에
|
|
1380
|
+
> `payment:read` / `subscription:manage` 를 opt-in 한 `service_role` 함수를 쓰세요.
|
|
1381
|
+
|
|
1382
|
+
```typescript
|
|
1383
|
+
// 결제수단 삭제 — PG 등록까지 지워졌는지는 프로바이더마다 다르다 (v6.1.0+)
|
|
1384
|
+
const deleted = await cb.subscription.deleteBillingKey('billing-key-1')
|
|
1385
|
+
if (!deleted.provider_revoked) {
|
|
1386
|
+
// toss 외 프로바이더: Connect Base 기록만 지워졌고 PG 등록은 남아 있다
|
|
1387
|
+
console.warn(deleted.provider_revoke_note)
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1333
1391
|
```typescript
|
|
1334
1392
|
// Create a subscription (정기 결제)
|
|
1335
1393
|
const subscription = await cb.subscription.create({
|
|
@@ -1384,6 +1442,60 @@ export async function handler(payload, ctx) {
|
|
|
1384
1442
|
얹힙니다. payapp/paypal 은 PG 가 결제일 변경 API 를 주지 않아 400 `next_billing_date_unsupported`
|
|
1385
1443
|
입니다 (로컬만 미루면 원래 날짜에 그대로 출금되므로 조용히 처리하지 않습니다).
|
|
1386
1444
|
|
|
1445
|
+
### Organizations (Teams / Workspaces)
|
|
1446
|
+
|
|
1447
|
+
앱의 **엔드유저가 만드는 조직**. Connect Base 콘솔의 협업자 RBAC(`cb.roles.*`)와는 별개
|
|
1448
|
+
시스템이다. **모든 조직 API 는 로그인한 회원 토큰을 요구한다** — 퍼블릭 키 단독 호출은 401 이다.
|
|
1449
|
+
|
|
1450
|
+
```typescript
|
|
1451
|
+
// 조직 만들기 (만든 사람이 owner)
|
|
1452
|
+
const org = await cb.organizations.create({ name: '우리 팀' })
|
|
1453
|
+
|
|
1454
|
+
// 초대 — 평문 토큰은 이 응답에서만 볼 수 있고, 메일 발송은 앱이 직접 한다
|
|
1455
|
+
const { token } = await cb.organizations.createInvitation(org.id, {
|
|
1456
|
+
email: 'teammate@example.com',
|
|
1457
|
+
role: 'member', // owner | admin | member
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
// 초대 수락 (초대받은 계정으로 로그인한 상태에서)
|
|
1461
|
+
await cb.organizations.acceptInvitation(token)
|
|
1462
|
+
|
|
1463
|
+
// 내 조직 목록 (조직 전환 UI)
|
|
1464
|
+
const mine = await cb.organizations.listMine()
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
#### 조직 컨텍스트는 10분마다 갱신해야 한다
|
|
1468
|
+
|
|
1469
|
+
데이터베이스 보안 규칙(RLS)의 `auth.org_id` / `auth.org_role` 은 **토큰에 실린 조직 컨텍스트**
|
|
1470
|
+
에서 온다. 이 컨텍스트의 수명은 액세스 토큰(1시간)과 **독립적인 10분**이다. 만료되어도 토큰
|
|
1471
|
+
자체는 유효해 401 이 나지 않고, 조직 규칙만 fail-closed 로 거부되어 **"멀쩡하던 조회가 갑자기
|
|
1472
|
+
전부 막히는"** 증상으로만 드러난다.
|
|
1473
|
+
|
|
1474
|
+
```typescript
|
|
1475
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1476
|
+
|
|
1477
|
+
async function activate(orgId: string) {
|
|
1478
|
+
// 새 액세스 토큰을 받아 SDK 에 자동 적용한다
|
|
1479
|
+
const ctx = await cb.organizations.switchTo(orgId)
|
|
1480
|
+
clearTimeout(timer)
|
|
1481
|
+
// 만료 60초 전에 갱신
|
|
1482
|
+
timer = setTimeout(() => activate(orgId), Math.max(ctx.org_context_expires_in - 60, 30) * 1000)
|
|
1483
|
+
return ctx
|
|
1484
|
+
}
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
토큰 회전(refresh)이 일어나면 조직 컨텍스트는 의도적으로 사라지므로 `switchTo()` 를 다시
|
|
1488
|
+
호출해야 한다. 내가 속하지 않은 조직은 403 이 아니라 **404** 다 (조직 ID 열거 방지).
|
|
1489
|
+
|
|
1490
|
+
RLS 규칙에서 쓸 수 있는 축:
|
|
1491
|
+
|
|
1492
|
+
| 축 | 조직 컨텍스트가 없을 때 | 용도 |
|
|
1493
|
+
|---|---|---|
|
|
1494
|
+
| `auth.org_id` / `auth.org_role` | **에러 (fail-closed)** | `data.org_id == auth.org_id` 같은 스칼라 비교 |
|
|
1495
|
+
| `auth.has_org` | `false` (에러 안 남) | 조직 유무로 분기 |
|
|
1496
|
+
| `hasOrgRole('owner')` | `false` (에러 안 남) | 조직 역할 판정 (앱 전역 `hasRole()` 과 다름) |
|
|
1497
|
+
| `inOrg('<uuid>')` | `false` (에러 안 남) | 특정 조직인지, 인자 생략 시 컨텍스트 유무 |
|
|
1498
|
+
|
|
1387
1499
|
### Support (End-user Issue Reporting)
|
|
1388
1500
|
|
|
1389
1501
|
End-user 가 앱 운영자에게 직접 버그·질문·요청을 발행하는 채널. 운영자 콘솔의 inbox 에 들어가며, AI 가 자동으로 요약·긴급도·카테고리를 분류한다 (운영자가 AI config 등록 시).
|
package/dist/cli.js
CHANGED
|
@@ -41,7 +41,8 @@ __export(cli_exports, {
|
|
|
41
41
|
normalizeRelativePath: () => normalizeRelativePath,
|
|
42
42
|
parseArgs: () => parseArgs,
|
|
43
43
|
registerEndpointBinding: () => registerEndpointBinding,
|
|
44
|
-
sha256Hex: () => sha256Hex
|
|
44
|
+
sha256Hex: () => sha256Hex,
|
|
45
|
+
storageAdminHeaders: () => storageAdminHeaders
|
|
45
46
|
});
|
|
46
47
|
module.exports = __toCommonJS(cli_exports);
|
|
47
48
|
var crypto = __toESM(require("crypto"));
|
|
@@ -295,6 +296,41 @@ function normalizeRelativePath(relativePath) {
|
|
|
295
296
|
if (!p.startsWith("/")) p = `/${p}`;
|
|
296
297
|
return p;
|
|
297
298
|
}
|
|
299
|
+
function storageAdminHeaders(config) {
|
|
300
|
+
const headers = {
|
|
301
|
+
"X-Public-Key": config.publicKey
|
|
302
|
+
};
|
|
303
|
+
if (config.secretKey) {
|
|
304
|
+
headers["X-User-Secret-Key"] = config.secretKey;
|
|
305
|
+
headers.Authorization = `Bearer ${config.secretKey}`;
|
|
306
|
+
return headers;
|
|
307
|
+
}
|
|
308
|
+
warn("Secret Key(cb_sk_)\uAC00 \uC124\uC815\uB3FC \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
|
|
309
|
+
log(
|
|
310
|
+
" Public Key \uB294 \uBE0C\uB77C\uC6B0\uC800 \uBC88\uB4E4\uC5D0 \uACF5\uAC1C\uB418\uB294 \uAC12\uC774\uB77C \uBC30\uD3EC/\uC2B9\uACA9\uC758 \uC790\uACA9\uC99D\uBA85\uC774 \uB420 \uC218 \uC5C6\uC5B4,"
|
|
311
|
+
);
|
|
312
|
+
log(" \uACE7 \uC11C\uBC84\uAC00 Secret Key \uB97C \uC694\uAD6C\uD569\uB2C8\uB2E4. \uC9C0\uAE08 \uC124\uC815\uD574 \uB450\uC138\uC694.");
|
|
313
|
+
log(
|
|
314
|
+
` ${colors.cyan}npx connectbase init${colors.reset} \uC774 \uBC1C\uAE09\uD574 .connectbaserc \uC5D0 \uC800\uC7A5\uD569\uB2C8\uB2E4.`
|
|
315
|
+
);
|
|
316
|
+
log(
|
|
317
|
+
` \uC774\uBBF8 \uD0A4\uAC00 \uC788\uB2E4\uBA74 ${colors.cyan}CONNECTBASE_SECRET_KEY${colors.reset} \uD658\uACBD\uBCC0\uC218\uB85C \uC9C0\uC815\uD558\uC138\uC694.`
|
|
318
|
+
);
|
|
319
|
+
log(" CI \uC5D0\uC11C\uB294 \uC800\uC7A5\uC18C \uC2DC\uD06C\uB9BF\uC5D0 \uB123\uC73C\uC138\uC694 \u2014 \uC800\uC7A5\uC18C\uC5D0 \uCEE4\uBC0B\uD558\uBA74 \uC548 \uB429\uB2C8\uB2E4.");
|
|
320
|
+
return headers;
|
|
321
|
+
}
|
|
322
|
+
function reportDeployAuthError(status, data) {
|
|
323
|
+
if (status !== 401) return false;
|
|
324
|
+
const detail = data?.error;
|
|
325
|
+
if (detail?.code !== "SECRET_KEY_REQUIRED") return false;
|
|
326
|
+
error(detail.message || "\uC774 \uC694\uCCAD\uC5D0\uB294 Secret Key \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
327
|
+
if (detail.suggestion) {
|
|
328
|
+
log("");
|
|
329
|
+
log(` ${detail.suggestion}`);
|
|
330
|
+
}
|
|
331
|
+
log("");
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
298
334
|
function loadConfig() {
|
|
299
335
|
const config = {
|
|
300
336
|
publicKey: process.env.CONNECTBASE_PUBLIC_KEY,
|
|
@@ -670,7 +706,7 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
|
|
|
670
706
|
);
|
|
671
707
|
}
|
|
672
708
|
const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
|
|
673
|
-
const headers =
|
|
709
|
+
const headers = storageAdminHeaders(config);
|
|
674
710
|
const timeoutMs = computeDeployTimeout(totalSize, deployOpts.timeoutMs);
|
|
675
711
|
log(
|
|
676
712
|
`${colors.dim}\uD0C0\uC784\uC544\uC6C3: ${Math.round(timeoutMs / 1e3)}\uCD08${colors.reset}`
|
|
@@ -897,13 +933,15 @@ ${colors.cyan}URL: ${data.url}${colors.reset}
|
|
|
897
933
|
} else {
|
|
898
934
|
const data = response.data;
|
|
899
935
|
const errorMsg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : typeof data === "string" ? data : `HTTP ${response.status}`;
|
|
900
|
-
|
|
936
|
+
if (!reportDeployAuthError(response.status, response.data)) {
|
|
937
|
+
error(`\uBC30\uD3EC \uC2E4\uD328: ${errorMsg}`);
|
|
938
|
+
}
|
|
901
939
|
process.exit(1);
|
|
902
940
|
}
|
|
903
941
|
}
|
|
904
942
|
async function promote(config, deploymentId) {
|
|
905
943
|
const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
|
|
906
|
-
const headers =
|
|
944
|
+
const headers = storageAdminHeaders(config);
|
|
907
945
|
info(
|
|
908
946
|
deploymentId ? `release ${deploymentId} \uB97C production \uC73C\uB85C \uC2B9\uACA9\uD569\uB2C8\uB2E4...` : "\uCD5C\uC2E0 \uBC30\uD3EC\uB97C production \uC73C\uB85C \uC2B9\uACA9\uD569\uB2C8\uB2E4..."
|
|
909
947
|
);
|
|
@@ -925,7 +963,9 @@ async function promote(config, deploymentId) {
|
|
|
925
963
|
if (res.status < 200 || res.status >= 300) {
|
|
926
964
|
const data2 = res.data;
|
|
927
965
|
const msg = typeof data2 === "object" && data2 !== null ? data2.error || data2.message || JSON.stringify(data2) : `HTTP ${res.status}`;
|
|
928
|
-
|
|
966
|
+
if (!reportDeployAuthError(res.status, res.data)) {
|
|
967
|
+
error(`promote \uC2E4\uD328: ${msg}`);
|
|
968
|
+
}
|
|
929
969
|
process.exit(1);
|
|
930
970
|
}
|
|
931
971
|
const data = res.data;
|
|
@@ -938,7 +978,7 @@ ${colors.cyan}Production: ${data.production_url}${colors.reset}
|
|
|
938
978
|
}
|
|
939
979
|
async function listReleases(config) {
|
|
940
980
|
const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
|
|
941
|
-
const headers =
|
|
981
|
+
const headers = storageAdminHeaders(config);
|
|
942
982
|
const res = await makeRequest(
|
|
943
983
|
`${baseStorageUrl}/deployments`,
|
|
944
984
|
"GET",
|
|
@@ -947,7 +987,9 @@ async function listReleases(config) {
|
|
|
947
987
|
if (res.status < 200 || res.status >= 300) {
|
|
948
988
|
const data2 = res.data;
|
|
949
989
|
const msg = typeof data2 === "object" && data2 !== null ? data2.error || data2.message || JSON.stringify(data2) : `HTTP ${res.status}`;
|
|
950
|
-
|
|
990
|
+
if (!reportDeployAuthError(res.status, res.data)) {
|
|
991
|
+
error(`\uBC30\uD3EC \uC774\uB825 \uC870\uD68C \uC2E4\uD328: ${msg}`);
|
|
992
|
+
}
|
|
951
993
|
process.exit(1);
|
|
952
994
|
}
|
|
953
995
|
const data = res.data;
|
|
@@ -1391,7 +1433,11 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `
|
|
|
1391
1433
|
const listRes = await makeRequest(
|
|
1392
1434
|
`${DEFAULT_BASE_URL}/v1/public/storages/webs`,
|
|
1393
1435
|
"GET",
|
|
1394
|
-
{
|
|
1436
|
+
{
|
|
1437
|
+
"X-Public-Key": publicKeyForSdk,
|
|
1438
|
+
"X-User-Secret-Key": secretKey,
|
|
1439
|
+
Authorization: `Bearer ${secretKey}`
|
|
1440
|
+
}
|
|
1395
1441
|
);
|
|
1396
1442
|
if (listRes.status === 200) {
|
|
1397
1443
|
const listData = listRes.data;
|
|
@@ -1429,7 +1475,11 @@ ${colors.blue}?${colors.reset} \uC120\uD0DD (\uBC88\uD638): `
|
|
|
1429
1475
|
const createRes = await makeRequest(
|
|
1430
1476
|
`${DEFAULT_BASE_URL}/v1/public/storages/webs`,
|
|
1431
1477
|
"POST",
|
|
1432
|
-
{
|
|
1478
|
+
{
|
|
1479
|
+
"X-Public-Key": publicKeyForSdk,
|
|
1480
|
+
"X-User-Secret-Key": secretKey,
|
|
1481
|
+
Authorization: `Bearer ${secretKey}`
|
|
1482
|
+
},
|
|
1433
1483
|
JSON.stringify({ name })
|
|
1434
1484
|
);
|
|
1435
1485
|
if (createRes.status !== 200) {
|
|
@@ -3434,7 +3484,7 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
|
|
|
3434
3484
|
|
|
3435
3485
|
${colors.yellow}\uD658\uACBD\uBCC0\uC218:${colors.reset}
|
|
3436
3486
|
CONNECTBASE_PUBLIC_KEY Public Key (cb_pk_*)
|
|
3437
|
-
CONNECTBASE_SECRET_KEY Secret Key (cb_sk_
|
|
3487
|
+
CONNECTBASE_SECRET_KEY Secret Key (cb_sk_*) \u2014 \uBC30\uD3EC/\uC2B9\uACA9/\uD130\uB110\uC5D0 \uD544\uC218
|
|
3438
3488
|
CONNECTBASE_PUBLIC_KEY (deprecated) \uB808\uAC70\uC2DC \u2014 publicKey \uB610\uB294 secretKey \uB85C \uC0AC\uC6A9
|
|
3439
3489
|
CONNECTBASE_STORAGE_ID \uC2A4\uD1A0\uB9AC\uC9C0 ID
|
|
3440
3490
|
CONNECTBASE_BASE_URL \uC11C\uBC84 URL
|
|
@@ -3672,5 +3722,6 @@ main().catch((err) => {
|
|
|
3672
3722
|
normalizeRelativePath,
|
|
3673
3723
|
parseArgs,
|
|
3674
3724
|
registerEndpointBinding,
|
|
3675
|
-
sha256Hex
|
|
3725
|
+
sha256Hex,
|
|
3726
|
+
storageAdminHeaders
|
|
3676
3727
|
});
|