connectbase-client 3.25.0 → 3.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,9 +12,9 @@ pnpm add connectbase-client
12
12
  yarn add connectbase-client
13
13
  ```
14
14
 
15
- ## API Key Types
15
+ ## Key Types
16
16
 
17
- Connect Base provides **two types** of API Keys. Use the right key for your use case:
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 (`VITE_CONNECTBASE_API_KEY`) | **Public Key** (`cb_pk_`) | React, Vue, etc. |
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 API 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.
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
- apiKey: 'cb_pk_your-public-key'
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-client init
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 API Key
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-client deploy ./dist -s <storage-id> -k <api-key>
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
- | `--api-key <key>` | `-k` | API Key |
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-client tunnel 8084 -k <api-key>
179
+ npx connectbase tunnel 8084 -k <public-key>
140
180
 
141
181
  # With environment variable
142
- export CONNECTBASE_API_KEY=your-api-key
143
- npx connectbase-client tunnel 8084
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-client tunnel 7860 --timeout 300 --max-body 50
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
- "apiKey": "your-api-key",
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 CONNECTBASE_API_KEY=your-api-key
239
+ export CONNECTBASE_PUBLIC_KEY=your-public-key
183
240
  export CONNECTBASE_STORAGE_ID=your-storage-id
184
- npx connectbase-client deploy ./dist
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: 10)
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
- // Update data
465
- await cb.database.updateData('table-id', 'data-id', {
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
- await cb.database.batch([
525
- { type: 'create', table: 'orders', data: { product: 'A', qty: 1 } },
526
- { type: 'update', table: 'inventory', doc_id: 'item-a', operators: {
527
- qty: { type: 'increment', value: -1 }
528
- }},
529
- { type: 'update', table: 'stats', doc_id: 'daily', operators: {
530
- order_count: { type: 'increment', value: 1 },
531
- last_order: { type: 'serverTimestamp' }
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
- await cb.database.transaction(
537
- [{ table: 'accounts', doc_id: 'user-1', alias: 'sender' }],
538
- [{ type: 'update', table: 'accounts', doc_id: 'user-1', operators: {
539
- balance: { type: 'increment', value: -100 }
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 push notifications
768
- await cb.push.register({
769
- token: 'fcm-token-or-apns-token',
770
- platform: 'android' // or 'ios', 'web'
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 topics
774
- await cb.push.subscribeToTopic('news')
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
- // Unsubscribe from topic
777
- await cb.push.unsubscribeFromTopic('news')
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
- // API Key/JWT 유효성 사전 검증
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 gameClient.connect()
1371
+ await cb.auth.signInMember({ login_id, password })
880
1372
  } catch (error) {
881
- if (error instanceof Error) {
882
- console.error('Connection failed:', error.message)
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
- // Or use event handlers
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({ apiKey: 'your-api-key' })
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