connectbase-client 3.25.0 → 3.26.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 +1929 -0
- package/LICENSE +21 -0
- package/MIGRATION-v2.md +149 -0
- package/README.md +575 -52
- package/dist/cli.js +1760 -324
- package/dist/connect-base.umd.js +4 -2
- package/dist/index.d.mts +3270 -336
- package/dist/index.d.ts +3270 -336
- package/dist/index.js +4133 -949
- package/dist/index.mjs +4125 -948
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ pnpm add connectbase-client
|
|
|
12
12
|
yarn add connectbase-client
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Key Types
|
|
16
16
|
|
|
17
|
-
Connect Base provides **two types** of
|
|
17
|
+
Connect Base provides **two types** of Keys. Use the right key for your use case:
|
|
18
18
|
|
|
19
19
|
| Type | Prefix | Use For | Permissions | Safe to Expose? |
|
|
20
20
|
|------|--------|---------|-------------|-----------------|
|
|
@@ -26,7 +26,7 @@ Connect Base provides **two types** of API Keys. Use the right key for your use
|
|
|
26
26
|
| Context | Key Type | Example |
|
|
27
27
|
|---------|----------|---------|
|
|
28
28
|
| Frontend SDK (`new ConnectBase()`) | **Public Key** (`cb_pk_`) | Web/app: DB queries, auth, file uploads |
|
|
29
|
-
| `.env` file (`
|
|
29
|
+
| `.env` file (`VITE_CONNECTBASE_PUBLIC_KEY`) | **Public Key** (`cb_pk_`) | React, Vue, etc. |
|
|
30
30
|
| CLI deploy (`.connectbaserc`) | **Public Key** (`cb_pk_`) | `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 |
|
|
@@ -35,7 +35,30 @@ Connect Base provides **two types** of API Keys. Use the right key for your use
|
|
|
35
35
|
>
|
|
36
36
|
> ⚠️ **Never use Secret Keys in frontend code** — RLS is bypassed, exposing all data.
|
|
37
37
|
|
|
38
|
-
Create
|
|
38
|
+
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
|
+
|
|
40
|
+
#### Server-side admin context (v3.22.0+)
|
|
41
|
+
|
|
42
|
+
When you create the SDK with **both** `publicKey` and `secretKey`, the client
|
|
43
|
+
attaches `X-Public-Key` (app identity) and `Authorization: Bearer cb_sk_*`
|
|
44
|
+
(privilege escalation) on every request. The server's `OptionalAdminSecretKey`
|
|
45
|
+
middleware verifies the secret key, sets an admin context, and **skips RLS**
|
|
46
|
+
for that request — useful for backend sync scripts, admin tools, and
|
|
47
|
+
`cb.auth.adminUpdateMember()`.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// SERVER-SIDE ONLY — never ship this in a browser/mobile bundle
|
|
51
|
+
const cb = new ConnectBase({
|
|
52
|
+
publicKey: process.env.CB_PUBLIC_KEY!, // cb_pk_
|
|
53
|
+
secretKey: process.env.CB_SECRET_KEY!, // cb_sk_ (admin)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Bypasses RLS .write/.read rules
|
|
57
|
+
await cb.database.createData('orders', { ... })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Without `secretKey`, every request is RLS-evaluated as normal — there is no
|
|
61
|
+
behavioral change for browser clients.
|
|
39
62
|
|
|
40
63
|
## Quick Start
|
|
41
64
|
|
|
@@ -44,7 +67,7 @@ import ConnectBase from 'connectbase-client'
|
|
|
44
67
|
|
|
45
68
|
// Initialize the SDK — use a Public Key (cb_pk_)
|
|
46
69
|
const cb = new ConnectBase({
|
|
47
|
-
|
|
70
|
+
publicKey: 'cb_pk_your-public-key'
|
|
48
71
|
})
|
|
49
72
|
|
|
50
73
|
// Create a game room client
|
|
@@ -65,8 +88,20 @@ gameClient
|
|
|
65
88
|
await gameClient.connect()
|
|
66
89
|
const state = await gameClient.createRoom({
|
|
67
90
|
maxPlayers: 4,
|
|
68
|
-
tickRate: 64
|
|
91
|
+
tickRate: 64,
|
|
92
|
+
scriptName: 'my-script', // Optional: attach a lua script (must be pre-uploaded + active)
|
|
69
93
|
})
|
|
94
|
+
|
|
95
|
+
// 3.14+ — Attached script 의 이름/버전을 검증하고 싶으면 createRoomDetailed 사용
|
|
96
|
+
import { GameError } from 'connectbase-client'
|
|
97
|
+
try {
|
|
98
|
+
const r = await gameClient.createRoomDetailed({ scriptName: 'my-script' })
|
|
99
|
+
console.log('attached', r.scriptName, 'v', r.scriptVersion)
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (e instanceof GameError && e.code === 'SCRIPT_NOT_FOUND') {
|
|
102
|
+
console.error('script missing — candidates:', e.available)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
70
105
|
```
|
|
71
106
|
|
|
72
107
|
## Features
|
|
@@ -79,6 +114,9 @@ const state = await gameClient.createRoom({
|
|
|
79
114
|
- **WebRTC**: Real-time audio/video communication
|
|
80
115
|
- **Payments**: Subscription and one-time payment support
|
|
81
116
|
- **AI Streaming**: Real-time AI text generation via WebSocket (Gemini)
|
|
117
|
+
- **Knowledge Base (RAG)**: Document indexing + BM25 search with nori 한국어 형태소. PDF / DOCX / text file upload via `addDocumentFromFile`
|
|
118
|
+
- **Endpoint**: Call your own GPU models on your own PC through one `cb_pk_*` key — ConnectBase forwards the payload as-is (dumb pipe)
|
|
119
|
+
- **Support**: End-user feedback/issue reporting — users send issues to app operators, AI auto-classifies summary/urgency/category
|
|
82
120
|
- **CLI**: Command-line tool for deploying web storage and tunneling local services
|
|
83
121
|
|
|
84
122
|
## CLI
|
|
@@ -89,14 +127,14 @@ Deploy your web application to Connect Base Web Storage with a single command.
|
|
|
89
127
|
|
|
90
128
|
```bash
|
|
91
129
|
# 1. Initialize (one-time setup)
|
|
92
|
-
npx connectbase
|
|
130
|
+
npx connectbase init
|
|
93
131
|
|
|
94
132
|
# 2. Deploy
|
|
95
133
|
npm run deploy
|
|
96
134
|
```
|
|
97
135
|
|
|
98
136
|
The `init` command will:
|
|
99
|
-
- Ask for your
|
|
137
|
+
- Ask for your Public Key
|
|
100
138
|
- List existing web storages or create a new one automatically
|
|
101
139
|
- Create a `.connectbaserc` config file
|
|
102
140
|
- Add `.connectbaserc` to `.gitignore`
|
|
@@ -115,7 +153,7 @@ The `init` command will:
|
|
|
115
153
|
If you prefer not to use `init`, you can pass options directly:
|
|
116
154
|
|
|
117
155
|
```bash
|
|
118
|
-
npx connectbase
|
|
156
|
+
npx connectbase deploy ./dist -s <storage-id> -k <public-key>
|
|
119
157
|
```
|
|
120
158
|
|
|
121
159
|
### Options
|
|
@@ -123,10 +161,12 @@ npx connectbase-client deploy ./dist -s <storage-id> -k <api-key>
|
|
|
123
161
|
| Option | Alias | Description |
|
|
124
162
|
|--------|-------|-------------|
|
|
125
163
|
| `--storage <id>` | `-s` | Storage ID |
|
|
126
|
-
| `--
|
|
164
|
+
| `--public-key <key>` | `-k` | API Key |
|
|
127
165
|
| `--base-url <url>` | `-u` | Custom server URL |
|
|
128
166
|
| `--timeout <sec>` | `-t` | Tunnel request timeout in seconds (tunnel only) |
|
|
129
167
|
| `--max-body <MB>` | | Tunnel max body size in MB (tunnel only) |
|
|
168
|
+
| `--label <name>` | | Auto-register the issued tunnel as an endpoint binding (tunnel only). Requires Secret Key. SDK callers can then use `cb.endpoint.call(label, …)` |
|
|
169
|
+
| `--description <text>` | | Endpoint binding description (only valid with `--label`) |
|
|
130
170
|
| `--help` | `-h` | Show help |
|
|
131
171
|
| `--version` | `-v` | Show version |
|
|
132
172
|
|
|
@@ -136,14 +176,14 @@ Expose a local server to the internet through a secure WebSocket tunnel. Useful
|
|
|
136
176
|
|
|
137
177
|
```bash
|
|
138
178
|
# Expose local port 8084 to the internet
|
|
139
|
-
npx connectbase
|
|
179
|
+
npx connectbase tunnel 8084 -k <public-key>
|
|
140
180
|
|
|
141
181
|
# With environment variable
|
|
142
|
-
export
|
|
143
|
-
npx connectbase
|
|
182
|
+
export CONNECTBASE_PUBLIC_KEY=your-public-key
|
|
183
|
+
npx connectbase tunnel 8084
|
|
144
184
|
|
|
145
185
|
# For GPU servers or long-running tasks (e.g., image generation)
|
|
146
|
-
npx connectbase
|
|
186
|
+
npx connectbase tunnel 7860 --timeout 300 --max-body 50
|
|
147
187
|
```
|
|
148
188
|
|
|
149
189
|
The tunnel creates a public URL like `https://tunnel.connectbase.world/<tunnel-id>/` that proxies all HTTP requests to your local service.
|
|
@@ -164,13 +204,30 @@ Features:
|
|
|
164
204
|
- Graceful shutdown with Ctrl+C
|
|
165
205
|
- No external dependencies (uses Node.js built-in modules)
|
|
166
206
|
|
|
207
|
+
#### Auto-register an endpoint binding (`--label`)
|
|
208
|
+
|
|
209
|
+
For workflows where you want the SDK to call your local model by a stable name
|
|
210
|
+
(`cb.endpoint.call("comfyui-main", …)`) instead of a random tunnel URL, pass
|
|
211
|
+
`--label <name>`. The CLI registers the issued `tunnel_id` as an endpoint binding
|
|
212
|
+
on the server, so your SDK only needs the Public Key.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Start ComfyUI on port 8188, expose it as endpoint label "comfyui-main"
|
|
216
|
+
npx connectbase tunnel 8188 --label comfyui-main --description "ComfyUI on my desktop"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Authentication uses your User Secret Key (`cb_sk_*`); the CLI calls the dual-auth
|
|
220
|
+
route `POST /v1/apps/:appID/endpoints/cli`. If the label already exists, the CLI
|
|
221
|
+
warns and keeps the tunnel running — update the binding to the new `tunnel_id`
|
|
222
|
+
from the console if needed.
|
|
223
|
+
|
|
167
224
|
### Configuration File
|
|
168
225
|
|
|
169
226
|
The `init` command creates `.connectbaserc` automatically. You can also create it manually:
|
|
170
227
|
|
|
171
228
|
```json
|
|
172
229
|
{
|
|
173
|
-
"
|
|
230
|
+
"publicKey": "your-public-key",
|
|
174
231
|
"storageId": "your-storage-id",
|
|
175
232
|
"deployDir": "./dist"
|
|
176
233
|
}
|
|
@@ -179,9 +236,9 @@ The `init` command creates `.connectbaserc` automatically. You can also create i
|
|
|
179
236
|
### Environment Variables
|
|
180
237
|
|
|
181
238
|
```bash
|
|
182
|
-
export
|
|
239
|
+
export CONNECTBASE_PUBLIC_KEY=your-public-key
|
|
183
240
|
export CONNECTBASE_STORAGE_ID=your-storage-id
|
|
184
|
-
npx connectbase
|
|
241
|
+
npx connectbase deploy ./dist
|
|
185
242
|
```
|
|
186
243
|
|
|
187
244
|
### Requirements
|
|
@@ -209,6 +266,43 @@ curl -X PUT "https://api.connectbase.world/v1/apps/{appID}/storages/webs/{storag
|
|
|
209
266
|
|
|
210
267
|
### Game Server
|
|
211
268
|
|
|
269
|
+
#### `cb.game.config` — Feature Opt-in (v3.1+, SDK 3.3.0+)
|
|
270
|
+
|
|
271
|
+
The game server's 7 features (`matchqueue` / `leaderboard` / `entity` / `scripts` /
|
|
272
|
+
`voice` / `replay` / `spectator`) are **explicit opt-in per app** as of v3.1
|
|
273
|
+
(2026-04-30). Disabled features return HTTP **403 `feature_disabled`**.
|
|
274
|
+
New apps default to all-OFF; existing apps without a config row fall back to
|
|
275
|
+
all-ON for compatibility.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Inspect current toggles
|
|
279
|
+
const cfg = await cb.game.config.get(appId)
|
|
280
|
+
// → { matchqueue_enabled, leaderboard_enabled, entity_enabled, scripts_enabled,
|
|
281
|
+
// voice_enabled, replay_enabled, spectator_enabled }
|
|
282
|
+
|
|
283
|
+
// Partial update — only the keys you send are applied; others are preserved.
|
|
284
|
+
await cb.game.config.set(appId, {
|
|
285
|
+
matchqueue_enabled: true,
|
|
286
|
+
leaderboard_enabled: true,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// Single-toggle convenience wrappers
|
|
290
|
+
await cb.game.config.enable(appId, 'matchqueue_enabled')
|
|
291
|
+
await cb.game.config.disable(appId, 'voice_enabled')
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The PATCH publishes a NATS invalidation so game-server caches drop the entry
|
|
295
|
+
immediately (30s TTL is the safety net).
|
|
296
|
+
|
|
297
|
+
| HTTP code | Body `error` | Meaning | Client action |
|
|
298
|
+
|-----------|--------------|---------|---------------|
|
|
299
|
+
| **403** | `feature_disabled` | Feature is not enabled for this app | Toggle via console or `cb.game.config.set(...)` |
|
|
300
|
+
| **429** | `cap_exceeded` | Per-app cardinality cap reached | Remove old rows or ask the operator to raise the cap env |
|
|
301
|
+
| **402** | `quota_exceeded` | Plan limit reached on a write route | Upgrade plan |
|
|
302
|
+
|
|
303
|
+
See [docs/game-server/OPT_IN.md](https://github.com/connectbase-world/connectbase/blob/release/docs/game-server/OPT_IN.md)
|
|
304
|
+
for the full policy.
|
|
305
|
+
|
|
212
306
|
#### GameRoom
|
|
213
307
|
|
|
214
308
|
The main class for real-time game communication.
|
|
@@ -263,8 +357,11 @@ Create a new game room.
|
|
|
263
357
|
const state = await gameClient.createRoom({
|
|
264
358
|
roomId: 'my-custom-room', // Optional: Custom room ID
|
|
265
359
|
categoryId: 'battle-royale', // Optional: Room category
|
|
266
|
-
maxPlayers: 100, // Optional: Max players (default:
|
|
360
|
+
maxPlayers: 100, // Optional: Max players (default: 100)
|
|
267
361
|
tickRate: 64, // Optional: Server tick rate (default: 64)
|
|
362
|
+
scriptName: 'main', // Optional (3.11.0+): Lua script attached to the room
|
|
363
|
+
// (uploaded+activated via console or POST /v1/game/:appID/scripts).
|
|
364
|
+
// Required for onTick / onPlayerJoin / onAction etc. to fire.
|
|
268
365
|
metadata: { map: 'forest' } // Optional: Custom metadata
|
|
269
366
|
})
|
|
270
367
|
```
|
|
@@ -366,6 +463,12 @@ gameClient
|
|
|
366
463
|
.on('onChat', (message: ChatMessage) => {
|
|
367
464
|
// Called when a chat message is received
|
|
368
465
|
})
|
|
466
|
+
.on('onMessage', (msg) => {
|
|
467
|
+
// Called for custom broadcast messages from the server-side Lua script
|
|
468
|
+
// (room.broadcast / room.send_to). Standard types (delta/chat/...) go to
|
|
469
|
+
// their dedicated handlers; only unknown `type` messages arrive here.
|
|
470
|
+
// Branch on msg.type for game-specific protocols.
|
|
471
|
+
})
|
|
369
472
|
.on('onError', (error: ErrorMessage) => {
|
|
370
473
|
// Called on errors
|
|
371
474
|
})
|
|
@@ -435,11 +538,41 @@ const result = await cb.oauth.signInWithPopup('google', 'https://myapp.com/oauth
|
|
|
435
538
|
await cb.auth.signOut()
|
|
436
539
|
```
|
|
437
540
|
|
|
541
|
+
#### Admin: update another member (v3.22.0+)
|
|
542
|
+
|
|
543
|
+
Set another member's `nickname`, `role`, or `custom_data` from a server-side
|
|
544
|
+
admin context. Requires the SDK to be initialized with `secretKey` — calling
|
|
545
|
+
this without one throws synchronously. Self-update is rejected by the server.
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
// SERVER-SIDE ONLY — admin context required (publicKey + secretKey)
|
|
549
|
+
const cb = new ConnectBase({
|
|
550
|
+
publicKey: process.env.CB_PUBLIC_KEY!,
|
|
551
|
+
secretKey: process.env.CB_SECRET_KEY!,
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
// Grant role used by RLS `auth.role` expressions
|
|
555
|
+
await cb.auth.adminUpdateMember('019abc12-...', { role: 'editor' })
|
|
556
|
+
|
|
557
|
+
// Clear the role
|
|
558
|
+
await cb.auth.adminUpdateMember('019abc12-...', { role: '' })
|
|
559
|
+
|
|
560
|
+
// Multi-field update
|
|
561
|
+
await cb.auth.adminUpdateMember('019abc12-...', {
|
|
562
|
+
nickname: 'Alice',
|
|
563
|
+
role: 'admin',
|
|
564
|
+
custom_data: { level: 5 },
|
|
565
|
+
})
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
`role` is the only way to populate the RLS expression variable `auth.role` —
|
|
569
|
+
end-users can't set it on themselves through the public profile API.
|
|
570
|
+
|
|
438
571
|
### Database
|
|
439
572
|
|
|
440
573
|
```typescript
|
|
441
574
|
// Query data
|
|
442
|
-
const { data } = await cb.database.getData('table-id', {
|
|
575
|
+
const { data, total_count } = await cb.database.getData('table-id', {
|
|
443
576
|
where: { status: 'active' },
|
|
444
577
|
limit: 10
|
|
445
578
|
})
|
|
@@ -456,13 +589,20 @@ const { data } = await cb.database.getData('table-id', {
|
|
|
456
589
|
limit: 20
|
|
457
590
|
})
|
|
458
591
|
|
|
459
|
-
// Insert data
|
|
592
|
+
// Insert data — returns the created DataItem (id + data + created_at + updated_at)
|
|
460
593
|
const newItem = await cb.database.createData('table-id', {
|
|
461
594
|
data: { name: 'John', email: 'john@example.com' }
|
|
462
595
|
})
|
|
596
|
+
console.log(newItem.id) // use immediately for navigation / cache updates
|
|
463
597
|
|
|
464
|
-
//
|
|
465
|
-
await cb.database.
|
|
598
|
+
// Bulk insert — returns { created: DataItem[], total, success, failed? }
|
|
599
|
+
const bulk = await cb.database.createMany('table-id', [
|
|
600
|
+
{ data: { name: 'User1' } },
|
|
601
|
+
{ data: { name: 'User2' } }
|
|
602
|
+
])
|
|
603
|
+
|
|
604
|
+
// Update data — returns the updated DataItem with merged fields
|
|
605
|
+
const updated = await cb.database.updateData('table-id', 'data-id', {
|
|
466
606
|
data: { name: 'Jane' }
|
|
467
607
|
})
|
|
468
608
|
|
|
@@ -519,26 +659,41 @@ nearby.results.forEach(place => {
|
|
|
519
659
|
|
|
520
660
|
#### Batch & Transactions
|
|
521
661
|
|
|
662
|
+
`table_id` 는 항상 UUID. 콘솔/REST 로 생성한 테이블의 UUID 를 그대로 사용한다.
|
|
663
|
+
|
|
664
|
+
v3.12+ 부터 server 가 부분 실패(`success: false`)를 응답하면 SDK 가 첫 실패 op 의
|
|
665
|
+
error 메시지로 throw 한다 — silent success 회귀 방지 차원. 호출자는 try/catch 로 감싼다.
|
|
666
|
+
|
|
522
667
|
```typescript
|
|
523
668
|
// Batch: atomic multi-table operations
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
669
|
+
try {
|
|
670
|
+
const result = await cb.database.batch([
|
|
671
|
+
{ type: 'create', table_id: ORDERS_TABLE_ID, data: { product: 'A', qty: 1 } },
|
|
672
|
+
{ type: 'update', table_id: INVENTORY_TABLE_ID, doc_id: 'item-a', operators: {
|
|
673
|
+
qty: { type: 'increment', value: -1 }
|
|
674
|
+
}},
|
|
675
|
+
{ type: 'update', table_id: STATS_TABLE_ID, doc_id: 'daily', operators: {
|
|
676
|
+
order_count: { type: 'increment', value: 1 },
|
|
677
|
+
last_order: { type: 'serverTimestamp' }
|
|
678
|
+
}}
|
|
679
|
+
])
|
|
680
|
+
// result.success, result.results[i].{success, doc_id, error}
|
|
681
|
+
} catch (e) {
|
|
682
|
+
// RLS 거부 / 검증 실패 / table_id 오타 등 — 전체 batch 가 atomic 하게 롤백
|
|
683
|
+
console.error('batch failed:', (e as Error).message)
|
|
684
|
+
}
|
|
534
685
|
|
|
535
686
|
// Transaction: read-then-write with ACID guarantees
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
687
|
+
try {
|
|
688
|
+
await cb.database.transaction(
|
|
689
|
+
[{ table_id: ACCOUNTS_TABLE_ID, doc_id: 'user-1', alias: 'sender' }],
|
|
690
|
+
[{ type: 'update', table_id: ACCOUNTS_TABLE_ID, doc_id: 'user-1', operators: {
|
|
691
|
+
balance: { type: 'increment', value: -100 }
|
|
692
|
+
}}]
|
|
693
|
+
)
|
|
694
|
+
} catch (e) {
|
|
695
|
+
console.error('transaction failed:', (e as Error).message)
|
|
696
|
+
}
|
|
542
697
|
```
|
|
543
698
|
|
|
544
699
|
#### Populate (Relation Query / JOIN)
|
|
@@ -677,6 +832,76 @@ const { pages } = await cb.storage.listPageMetas('web-storage-id')
|
|
|
677
832
|
await cb.storage.deletePageMeta('web-storage-id', '/products/123')
|
|
678
833
|
```
|
|
679
834
|
|
|
835
|
+
### Knowledge Base (RAG)
|
|
836
|
+
|
|
837
|
+
문서를 등록하고 BM25 풀텍스트 검색으로 관련 청크를 찾는 RAG 인프라. 한국어는 nori 형태소 분석기 적용. AI 채팅에 컨텍스트로 연결하려면 `cb.ai.chatStream({ knowledgeBaseId })` 를 사용한다.
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
// 텍스트 문서 추가 (즉시 처리)
|
|
841
|
+
const doc = await cb.knowledge.addDocument('kb-id', {
|
|
842
|
+
name: '환불 정책',
|
|
843
|
+
source_type: 'text',
|
|
844
|
+
content: '환불은 구매 후 7일 이내 신청 가능합니다...',
|
|
845
|
+
metadata: { category: 'policy' }
|
|
846
|
+
})
|
|
847
|
+
// doc.status: 'pending' → 백그라운드 처리 후 'ready'
|
|
848
|
+
|
|
849
|
+
// URL 에서 가져오기
|
|
850
|
+
await cb.knowledge.addDocument('kb-id', {
|
|
851
|
+
name: '도움말',
|
|
852
|
+
source_type: 'url',
|
|
853
|
+
source_url: 'https://example.com/help.html',
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
// PDF / DOCX / text 파일 업로드 (3.17.0+)
|
|
857
|
+
// 브라우저: <input type="file"> 결과를 그대로 전달
|
|
858
|
+
const file = (document.querySelector('input[type=file]') as HTMLInputElement).files![0]
|
|
859
|
+
await cb.knowledge.addDocumentFromFile('kb-id', file, {
|
|
860
|
+
metadata: { tag: 'manual' },
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
// Node.js: fs.readFileSync 로 읽은 Buffer
|
|
864
|
+
import { readFileSync } from 'node:fs'
|
|
865
|
+
await cb.knowledge.addDocumentFromFile('kb-id', {
|
|
866
|
+
data: readFileSync('./report.pdf'),
|
|
867
|
+
mimeType: 'application/pdf',
|
|
868
|
+
name: 'report.pdf',
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
// 문서 목록 / 삭제
|
|
872
|
+
const { documents } = await cb.knowledge.listDocuments('kb-id')
|
|
873
|
+
await cb.knowledge.deleteDocument('kb-id', 'doc-id')
|
|
874
|
+
|
|
875
|
+
// 문서 수정 — content/file_content/metadata 변경 시 전체 재색인, name 만 보내면 라벨만 변경
|
|
876
|
+
await cb.knowledge.updateDocument('kb-id', 'doc-id', {
|
|
877
|
+
content: '환불은 구매 후 14일 이내에 가능합니다...',
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
// 키워드 검색 (BM25)
|
|
881
|
+
const results = await cb.knowledge.search('kb-id', {
|
|
882
|
+
query: '환불 정책이 어떻게 되나요?',
|
|
883
|
+
top_k: 5,
|
|
884
|
+
})
|
|
885
|
+
results.results.forEach(r => console.log(`[${r.score.toFixed(2)}] ${r.document_name}: ${r.content.slice(0, 80)}...`))
|
|
886
|
+
|
|
887
|
+
// Agentic Search — AI 가 다중 쿼리 자동 생성 (앱에 AI 프로바이더 설정 필요)
|
|
888
|
+
await cb.knowledge.search('kb-id', { query: '회원 등급별 혜택 비교', agentic: true })
|
|
889
|
+
|
|
890
|
+
// GET 방식 (간단한 사용)
|
|
891
|
+
await cb.knowledge.searchGet('kb-id', '환불', 3)
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
**파일 업로드 제약 (`addDocumentFromFile`)**
|
|
895
|
+
|
|
896
|
+
- 지원 MIME: `application/pdf` (텍스트 PDF), DOCX, `text/*` (plain/markdown/csv/html), `application/json`
|
|
897
|
+
- 미지원: 스캔 이미지 PDF / OCR / HWP / XLSX → `unsupported mime type for text extraction` 에러
|
|
898
|
+
- 크기 상한: **50MB** (원본 바이너리 기준)
|
|
899
|
+
- 추출 결과 빈 텍스트일 경우 `extracted text is empty` 에러 (스캔 PDF 등)
|
|
900
|
+
|
|
901
|
+
**사용자별 격리 (다중 사용자 RAG)**
|
|
902
|
+
|
|
903
|
+
`Authorization: Bearer <appmember-jwt>` 를 함께 보내면 서버가 자동으로 본인 metadata.user_id 로 결과를 제한하고, addDocument 시에도 자동 태깅. `search` 의 `where` 에 `'$auth.member_id'` 토큰 사용 시 서버가 인증된 AppMember ID 로 치환한다.
|
|
904
|
+
|
|
680
905
|
### Realtime
|
|
681
906
|
|
|
682
907
|
```typescript
|
|
@@ -701,6 +926,29 @@ await subscription.unsubscribe()
|
|
|
701
926
|
await cb.realtime.disconnect()
|
|
702
927
|
```
|
|
703
928
|
|
|
929
|
+
#### Presence / Typing
|
|
930
|
+
|
|
931
|
+
Presence(온라인 상태) 와 typing(입력 중 표시) 은 `cb.realtime.*` 가 단일 SoT 입니다.
|
|
932
|
+
v2.0.0 에서 `cb.database.setPresence` / `subscribePresence` 는 제거되었습니다.
|
|
933
|
+
v1.x 에서 마이그레이션은 [MIGRATION-v2.md](./MIGRATION-v2.md) 참고.
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
// 본인 온라인 상태 publish
|
|
937
|
+
await cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })
|
|
938
|
+
|
|
939
|
+
// 다른 사용자 상태 구독
|
|
940
|
+
const unsub = await cb.realtime.subscribePresence('user-id', (info) => {
|
|
941
|
+
console.log(info.userId, info.status, info.eventType) // join | leave | update
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
// 룸 단위 typing indicator
|
|
945
|
+
await cb.realtime.startTyping('room-id')
|
|
946
|
+
await cb.realtime.stopTyping('room-id')
|
|
947
|
+
const unsubTyping = await cb.realtime.onTypingChange('room-id', (typing) => {
|
|
948
|
+
console.log(typing.users) // 현재 입력 중인 사용자 ID 목록
|
|
949
|
+
})
|
|
950
|
+
```
|
|
951
|
+
|
|
704
952
|
#### AI Streaming
|
|
705
953
|
|
|
706
954
|
Real-time AI text generation using Gemini API through WebSocket.
|
|
@@ -761,26 +1009,241 @@ await session.stop()
|
|
|
761
1009
|
| `promptTokens` | `number` | Input prompt tokens |
|
|
762
1010
|
| `duration` | `number` | Generation time in ms |
|
|
763
1011
|
|
|
1012
|
+
### Server Functions
|
|
1013
|
+
|
|
1014
|
+
Invoke a deployed function from the SDK, or expose it as a raw HTTP webhook
|
|
1015
|
+
that external services (Discord, Stripe, GitHub, Slack Events, etc.) can call
|
|
1016
|
+
directly.
|
|
1017
|
+
|
|
1018
|
+
```typescript
|
|
1019
|
+
// Invoke a function (publicKey-authenticated; runs in your Knative pod)
|
|
1020
|
+
const result = await cb.functions.invoke('019abc12-...', { orderId: '...' })
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
#### Raw HTTP webhook URL (v3.22.0+)
|
|
1024
|
+
|
|
1025
|
+
For external SaaS webhooks where you can't customize the request shape (raw
|
|
1026
|
+
body, vendor-specific signature headers, arbitrary HTTP methods), enable
|
|
1027
|
+
`http_trigger_enabled` on the function (Console or MCP `update_function`) and
|
|
1028
|
+
register the URL returned by `getWebhookURL()` with the upstream service.
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
const url = cb.functions.getWebhookURL('019abc12-...')
|
|
1032
|
+
// → https://api.connectbase.world/v1/public/functions/019abc12-.../webhook
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
| `http_trigger_auth` | Required header | Use for |
|
|
1036
|
+
|---|---|---|
|
|
1037
|
+
| `none` | _(none)_ | External SaaS webhooks (function verifies signature itself) |
|
|
1038
|
+
| `public_key` | `X-Public-Key: cb_pk_*` | Your own clients/services |
|
|
1039
|
+
| `secret_key` | `Authorization: Bearer cb_sk_*` | Server-to-server admin calls |
|
|
1040
|
+
|
|
1041
|
+
The endpoint forwards the **raw request body** (no JSON wrap), preserves
|
|
1042
|
+
method/path/query, and forwards all headers — so signature checks (Ed25519,
|
|
1043
|
+
HMAC-SHA256, Stripe-Signature, X-Hub-Signature-256) work end-to-end. Body
|
|
1044
|
+
limit is 10MB.
|
|
1045
|
+
|
|
1046
|
+
Return `{ statusCode, headers, body }` from the handler to emit a custom
|
|
1047
|
+
HTTP response (for example, Discord Interactions requires a `200` with a
|
|
1048
|
+
JSON body within 3 seconds):
|
|
1049
|
+
|
|
1050
|
+
```javascript
|
|
1051
|
+
export async function handler(event, ctx) {
|
|
1052
|
+
// event.method / event.path / event.query / event.headers / event.body
|
|
1053
|
+
// are populated for webhook invocations.
|
|
1054
|
+
return {
|
|
1055
|
+
statusCode: 200,
|
|
1056
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1057
|
+
body: JSON.stringify({ type: 1 }), // Discord PING ack
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
### Endpoint (Local Model Tunnel)
|
|
1063
|
+
|
|
1064
|
+
`cb.endpoint.*` is a dumb pipe to your own GPU/model server running behind a
|
|
1065
|
+
ConnectBase tunnel. ConnectBase doesn't know your model, payload, or response
|
|
1066
|
+
shape — it routes a `cb_pk_*` call by label to the registered tunnel and forwards
|
|
1067
|
+
the body and headers as-is.
|
|
1068
|
+
|
|
1069
|
+
**Setup**: run `connectbase tunnel <port> --label <name>` once on the machine
|
|
1070
|
+
hosting the model (see [Tunnel](#tunnel)) — that registers the binding. Then any
|
|
1071
|
+
client with the app's Public Key can call it.
|
|
1072
|
+
|
|
1073
|
+
#### `cb.endpoint.call(label, init): Promise<Response>`
|
|
1074
|
+
|
|
1075
|
+
`fetch()`-compatible signature. Returns the raw `Response` — read the body as
|
|
1076
|
+
JSON, text, or stream as needed.
|
|
1077
|
+
|
|
1078
|
+
```typescript
|
|
1079
|
+
const cb = new ConnectBase({ publicKey: 'cb_pk_...' })
|
|
1080
|
+
|
|
1081
|
+
// ComfyUI prompt graph
|
|
1082
|
+
const res = await cb.endpoint.call('comfyui-main', {
|
|
1083
|
+
method: 'POST',
|
|
1084
|
+
path: '/prompt',
|
|
1085
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1086
|
+
body: JSON.stringify({ prompt: { /* ComfyUI node graph */ } }),
|
|
1087
|
+
})
|
|
1088
|
+
const data = await res.json()
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
```typescript
|
|
1092
|
+
// Streaming response (SSE / chunked) — vLLM chat completions
|
|
1093
|
+
const res = await cb.endpoint.call('vllm-local', {
|
|
1094
|
+
method: 'POST',
|
|
1095
|
+
path: '/v1/chat/completions',
|
|
1096
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1097
|
+
body: JSON.stringify({ stream: true, messages: [/* { role, content } */] }),
|
|
1098
|
+
})
|
|
1099
|
+
if (!res.body) throw new Error('no stream')
|
|
1100
|
+
const reader = res.body.getReader()
|
|
1101
|
+
while (true) {
|
|
1102
|
+
const { done, value } = await reader.read()
|
|
1103
|
+
if (done) break
|
|
1104
|
+
// value is a Uint8Array — decode and process chunk
|
|
1105
|
+
}
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
```typescript
|
|
1109
|
+
// Cancel an in-flight request
|
|
1110
|
+
const ctrl = new AbortController()
|
|
1111
|
+
setTimeout(() => ctrl.abort(), 30_000)
|
|
1112
|
+
await cb.endpoint.call('hunyuan-laptop', {
|
|
1113
|
+
method: 'POST',
|
|
1114
|
+
path: '/generate',
|
|
1115
|
+
signal: ctrl.signal,
|
|
1116
|
+
body: JSON.stringify({ /* model input */ }),
|
|
1117
|
+
})
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
**`EndpointCallInit`**
|
|
1121
|
+
|
|
1122
|
+
| Field | Type | Required | Description |
|
|
1123
|
+
|-------|------|----------|-------------|
|
|
1124
|
+
| `path` | `string` | yes | Path on your model server, must start with `/` (e.g. `/prompt`, `/v1/chat/completions`) |
|
|
1125
|
+
| `method` | `string` | no (`GET`) | HTTP method |
|
|
1126
|
+
| `headers` | `HeadersInit` | no | Extra request headers; `X-Public-Key` is auto-injected unless you set it |
|
|
1127
|
+
| `body` | `BodyInit \| null` | no | Request body — `string`, `Blob`, `ArrayBuffer`, `FormData`, or `ReadableStream` |
|
|
1128
|
+
| `signal` | `AbortSignal` | no | Abort signal for cancellation |
|
|
1129
|
+
|
|
1130
|
+
The SDK assembles the URL as `${baseUrl}/v1/proxy/${label}${path}` and forwards
|
|
1131
|
+
the request. Because the response is the raw `fetch` `Response`, streaming
|
|
1132
|
+
formats (SSE, chunked, NDJSON) work out of the box.
|
|
1133
|
+
|
|
1134
|
+
#### `cb.endpoint.pollUntil<T>(label, init, predicate, opts?): Promise<T>`
|
|
1135
|
+
|
|
1136
|
+
One-line "submit job → poll for result" pattern. Repeatedly calls the same
|
|
1137
|
+
endpoint until `predicate` returns a value. Designed for ComfyUI `/history/{id}`,
|
|
1138
|
+
A1111 `/sdapi/v1/progress`, or any custom queue API.
|
|
1139
|
+
|
|
1140
|
+
Behavior:
|
|
1141
|
+
- Calls `cb.endpoint.call(label, init)` and passes the parsed body to `predicate`
|
|
1142
|
+
- Returns `undefined` from `predicate` → wait `intervalMs` and retry
|
|
1143
|
+
- Returns a value from `predicate` → resolve immediately with that value
|
|
1144
|
+
- HTTP `5xx` / network error → retry. HTTP `4xx` → reject (job-level error)
|
|
1145
|
+
- `timeoutMs` exceeded or `signal` aborted → reject
|
|
1146
|
+
|
|
1147
|
+
```typescript
|
|
1148
|
+
type Hist = Record<
|
|
1149
|
+
string,
|
|
1150
|
+
{ outputs: Record<string, { images?: { filename: string }[] }> }
|
|
1151
|
+
>
|
|
1152
|
+
|
|
1153
|
+
const filename = await cb.endpoint.pollUntil<string>(
|
|
1154
|
+
'comfyui-main',
|
|
1155
|
+
{ path: `/history/${promptId}` },
|
|
1156
|
+
(data: Hist) => {
|
|
1157
|
+
const entry = data[promptId]
|
|
1158
|
+
if (!entry) return undefined // still queued
|
|
1159
|
+
for (const out of Object.values(entry.outputs)) {
|
|
1160
|
+
const img = out.images?.[0]
|
|
1161
|
+
if (img) return img.filename
|
|
1162
|
+
}
|
|
1163
|
+
return undefined
|
|
1164
|
+
},
|
|
1165
|
+
{ intervalMs: 1000, timeoutMs: 5 * 60_000 },
|
|
1166
|
+
)
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
**`PollUntilOptions`**
|
|
1170
|
+
|
|
1171
|
+
| Field | Type | Default | Description |
|
|
1172
|
+
|-------|------|---------|-------------|
|
|
1173
|
+
| `intervalMs` | `number` | `1500` | Poll interval in ms |
|
|
1174
|
+
| `timeoutMs` | `number` | `300000` (5 min) | Total timeout in ms — reject if exceeded |
|
|
1175
|
+
| `parse` | `'json' \| 'text' \| 'none'` | `'json'` | Body parser. `'json'` falls back to `undefined` on parse error |
|
|
1176
|
+
| `signal` | `AbortSignal` | — | External cancel signal — reject immediately on abort |
|
|
1177
|
+
|
|
1178
|
+
#### `cb.endpoint.url(label, path): string`
|
|
1179
|
+
|
|
1180
|
+
Returns the assembled call URL (`${baseUrl}/v1/proxy/${label}${path}`) for
|
|
1181
|
+
URL-passing scenarios where you control the request and can attach the
|
|
1182
|
+
`X-Public-Key` header yourself.
|
|
1183
|
+
|
|
1184
|
+
⚠️ **Browser-native APIs that cannot set custom headers will fail with `401`.**
|
|
1185
|
+
ConnectBase's proxy requires `X-Public-Key` on every call (header-only — no
|
|
1186
|
+
`?api_key=` fallback), so `<img src>`, `new Image()`, native `WebSocket`,
|
|
1187
|
+
`<script src>`, EventSource, etc. **cannot authenticate** through this URL.
|
|
1188
|
+
Use `cb.endpoint.call()` instead for those cases:
|
|
1189
|
+
|
|
1190
|
+
```typescript
|
|
1191
|
+
// ✅ Render an image: download via call(), then convert to a blob URL
|
|
1192
|
+
const res = await cb.endpoint.call('comfyui-main', {
|
|
1193
|
+
path: `/view?filename=${encodeURIComponent(name)}`,
|
|
1194
|
+
})
|
|
1195
|
+
img.src = URL.createObjectURL(await res.blob())
|
|
1196
|
+
// ...later: URL.revokeObjectURL(img.src)
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
For permanent images (works across CDN, survives tunnel restarts), upload the
|
|
1200
|
+
blob to `cb.storage` and use `saved.url` — see
|
|
1201
|
+
[`examples/ai-image-generator/`](https://github.com/connectbase-world/connectbase/tree/release/examples/ai-image-generator).
|
|
1202
|
+
|
|
1203
|
+
When `cb.endpoint.url()` IS the right tool:
|
|
1204
|
+
|
|
1205
|
+
- Logging / debugging the resolved tunnel URL
|
|
1206
|
+
- Passing the URL to a backend service or worker that will make the call with proper headers
|
|
1207
|
+
- Building a `RequestInfo` for a custom `fetch()` wrapper (you control headers)
|
|
1208
|
+
|
|
1209
|
+
```typescript
|
|
1210
|
+
console.log(cb.endpoint.url('comfyui-main', '/prompt'))
|
|
1211
|
+
// → https://api.connectbase.world/v1/proxy/comfyui-main/prompt
|
|
1212
|
+
|
|
1213
|
+
// Hand the URL to a Service Worker that injects X-Public-Key
|
|
1214
|
+
sw.postMessage({ url: cb.endpoint.url('comfyui-main', '/prompt'), key: PK })
|
|
1215
|
+
```
|
|
1216
|
+
|
|
764
1217
|
### Push Notifications
|
|
765
1218
|
|
|
766
1219
|
```typescript
|
|
767
|
-
// Register for
|
|
768
|
-
await cb.push.
|
|
769
|
-
|
|
770
|
-
platform: 'android' //
|
|
1220
|
+
// Register a device (FCM for Android, APNS for iOS)
|
|
1221
|
+
const device = await cb.push.registerDevice({
|
|
1222
|
+
device_token: 'fcm-token-or-apns-token',
|
|
1223
|
+
platform: 'android', // 'android' | 'ios' | 'web'
|
|
1224
|
+
device_name: 'Galaxy S24'
|
|
771
1225
|
})
|
|
772
1226
|
|
|
773
|
-
// Subscribe to
|
|
774
|
-
await cb.push.
|
|
1227
|
+
// Subscribe the device to a topic (deviceToken is required)
|
|
1228
|
+
await cb.push.subscribeTopic(device.device_token, 'news')
|
|
1229
|
+
|
|
1230
|
+
// Unsubscribe the device from a topic
|
|
1231
|
+
await cb.push.unsubscribeTopic(device.device_token, 'news')
|
|
775
1232
|
|
|
776
|
-
//
|
|
777
|
-
await cb.push.
|
|
1233
|
+
// Web Push (browsers)
|
|
1234
|
+
const vapidKey = await cb.push.getVAPIDPublicKey()
|
|
1235
|
+
const registration = await navigator.serviceWorker.ready
|
|
1236
|
+
const subscription = await registration.pushManager.subscribe({
|
|
1237
|
+
userVisibleOnly: true,
|
|
1238
|
+
applicationServerKey: vapidKey.public_key
|
|
1239
|
+
})
|
|
1240
|
+
await cb.push.registerWebPush(subscription)
|
|
778
1241
|
```
|
|
779
1242
|
|
|
780
1243
|
### WebRTC
|
|
781
1244
|
|
|
782
1245
|
```typescript
|
|
783
|
-
//
|
|
1246
|
+
// Public Key/JWT 유효성 사전 검증
|
|
784
1247
|
const result = await cb.webrtc.validate()
|
|
785
1248
|
if (result.valid) {
|
|
786
1249
|
console.log('인증 성공:', result.app_id)
|
|
@@ -821,6 +1284,33 @@ const status = await cb.subscription.getStatus()
|
|
|
821
1284
|
await cb.subscription.cancel()
|
|
822
1285
|
```
|
|
823
1286
|
|
|
1287
|
+
### Support (End-user Issue Reporting)
|
|
1288
|
+
|
|
1289
|
+
End-user 가 앱 운영자에게 직접 버그·질문·요청을 발행하는 채널. 운영자 콘솔의 inbox 에 들어가며, AI 가 자동으로 요약·긴급도·카테고리를 분류한다 (운영자가 AI config 등록 시).
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
// 로그인 사용자 (AppMember JWT 자동 첨부)
|
|
1293
|
+
await cb.support.reportIssue({
|
|
1294
|
+
title: "결제 화면이 멈춰요",
|
|
1295
|
+
body: "결제 버튼 클릭 후 로딩이 끝나지 않습니다.",
|
|
1296
|
+
category: "bug", // bug | question | feature_request | incident | other
|
|
1297
|
+
metadata: { pageUrl: window.location.href }
|
|
1298
|
+
})
|
|
1299
|
+
|
|
1300
|
+
// 익명 발행 + reCAPTCHA v3 (운영자가 RECAPTCHA_SECRET 설정한 경우 권장)
|
|
1301
|
+
const recaptchaToken = await grecaptcha.execute(SITE_KEY, { action: 'report_issue' })
|
|
1302
|
+
await cb.support.reportIssue({
|
|
1303
|
+
title: "...",
|
|
1304
|
+
body: "...",
|
|
1305
|
+
anonymousEmail: "user@example.com", // 회신 받을 이메일 (선택)
|
|
1306
|
+
recaptchaToken,
|
|
1307
|
+
})
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
응답: `{ id, status: 'open', created_at }` (보안상 최소 정보만).
|
|
1311
|
+
|
|
1312
|
+
발행자가 결과를 조회하는 채널은 후속 plan 에서 추가될 예정 — 현재는 운영자가 외부 webhook(이메일/Slack 등)으로 회신하는 방식 권장.
|
|
1313
|
+
|
|
824
1314
|
## Types
|
|
825
1315
|
|
|
826
1316
|
### GameState
|
|
@@ -875,20 +1365,53 @@ interface ConnectionState {
|
|
|
875
1365
|
## Error Handling
|
|
876
1366
|
|
|
877
1367
|
```typescript
|
|
1368
|
+
import ConnectBase, { ApiError, AuthError } from 'connectbase-client'
|
|
1369
|
+
|
|
878
1370
|
try {
|
|
879
|
-
await
|
|
1371
|
+
await cb.auth.signInMember({ login_id, password })
|
|
880
1372
|
} catch (error) {
|
|
881
|
-
if (error instanceof
|
|
882
|
-
|
|
1373
|
+
if (error instanceof ApiError) {
|
|
1374
|
+
// HTTP 응답 기반 에러: status/code/details 로 분기 가능
|
|
1375
|
+
if (error.statusCode === 429) {
|
|
1376
|
+
const details = error.details as { retry_after_seconds?: number } | undefined
|
|
1377
|
+
const retryAfter = details?.retry_after_seconds
|
|
1378
|
+
// ...
|
|
1379
|
+
}
|
|
1380
|
+
} else if (error instanceof AuthError) {
|
|
1381
|
+
// refresh 실패/토큰 만료
|
|
883
1382
|
}
|
|
884
1383
|
}
|
|
885
1384
|
|
|
886
|
-
//
|
|
1385
|
+
// Game API 는 별도 이벤트 핸들러도 지원
|
|
887
1386
|
gameClient.on('onError', (error) => {
|
|
888
1387
|
console.error('Game error:', error.message)
|
|
889
1388
|
})
|
|
890
1389
|
```
|
|
891
1390
|
|
|
1391
|
+
### 전역 에러 관찰자 (v1.9.0+)
|
|
1392
|
+
|
|
1393
|
+
`ConnectBase` 초기화 시 `onError` 옵션을 주면 모든 `ApiError` / `AuthError` 가 한 곳으로 모입니다. Sentry/Datadog 등 관측성 툴과 연결하기 쉽습니다.
|
|
1394
|
+
|
|
1395
|
+
```typescript
|
|
1396
|
+
const cb = new ConnectBase({
|
|
1397
|
+
publicKey: 'cb_pk_...',
|
|
1398
|
+
onError: (error) => {
|
|
1399
|
+
Sentry.captureException(error)
|
|
1400
|
+
},
|
|
1401
|
+
})
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
### 요청 타임아웃 (v1.9.0+)
|
|
1405
|
+
|
|
1406
|
+
기본 30초 타임아웃이 모든 HTTP 호출에 적용됩니다. `requestTimeoutMs` 로 전역 기본값을 바꾸거나, 0 이하 값을 주면 비활성화할 수 있습니다.
|
|
1407
|
+
|
|
1408
|
+
```typescript
|
|
1409
|
+
const cb = new ConnectBase({
|
|
1410
|
+
publicKey: 'cb_pk_...',
|
|
1411
|
+
requestTimeoutMs: 60000, // 60s
|
|
1412
|
+
})
|
|
1413
|
+
```
|
|
1414
|
+
|
|
892
1415
|
## Best Practices
|
|
893
1416
|
|
|
894
1417
|
### State Synchronization
|
|
@@ -939,7 +1462,7 @@ setInterval(async () => {
|
|
|
939
1462
|
```typescript
|
|
940
1463
|
import ConnectBase from 'connectbase-client'
|
|
941
1464
|
|
|
942
|
-
const cb = new ConnectBase({
|
|
1465
|
+
const cb = new ConnectBase({ publicKey: 'your-public-key' })
|
|
943
1466
|
const game = cb.game.createClient({ clientId: `player-${Date.now()}` })
|
|
944
1467
|
|
|
945
1468
|
// Local player state
|