connectbase-client 3.0.1 → 3.1.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 CHANGED
@@ -3,6 +3,34 @@
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
+ ## [3.1.0] - 2026-04-29
7
+
8
+ ### Added — Endpoint API (로컬 모델 터널 dumb pipe)
9
+
10
+ 사용자 PC GPU 모델 (ComfyUI / A1111 / Hunyuan3D / vLLM / 자체 FastAPI 등) 을
11
+ `cb_pk_*` 한 키로 호출하는 새 모듈. ConnectBase 는 모델·API·워크플로우를
12
+ 알지 않고, 라벨 → tunnel 매핑만 들고 페이로드/응답을 그대로 통과시킵니다.
13
+
14
+ - **`cb.endpoint.call(label, init)`** — fetch() 시그니처 호환 (path, method,
15
+ headers, body, signal). URL 은 `${baseUrl}/v1/proxy/${label}${path}` 로
16
+ 자동 조립, `X-Public-Key` 헤더 자동 주입. SSE / chunked 스트리밍은
17
+ `res.body.getReader()` 로 그대로 읽기.
18
+ - 신규 export: `EndpointAPI`, `EndpointCallInit`.
19
+
20
+ ### Added — CLI
21
+
22
+ - **`connectbase tunnel <port> --label <name>`** — tunnel 발급 후 endpoint
23
+ binding 을 자동 등록. SDK 사용자가 즉시 `cb.endpoint.call("<label>", { ... })`
24
+ 로 호출 가능. 인증은 User Secret Key (`cb_sk_*`) — dual-auth 라우트
25
+ `POST /v1/apps/:appID/endpoints/cli`.
26
+ - **`--description <text>`** — endpoint binding 의 설명 (`--label` 동반 시만).
27
+ - 이미 등록된 라벨이면 경고 후 진행 (다른 tunnel_id 로 갱신은 콘솔에서 PATCH).
28
+
29
+ ### Fixed
30
+
31
+ - JSDoc 안 중첩 블록주석으로 빌드가 깨지던 회귀 수정 (`*/` 가 주석을 조기
32
+ 종료시키던 케이스).
33
+
6
34
  ## [3.0.1] - 2026-04-28
7
35
 
8
36
  ### Fixed
package/README.md CHANGED
@@ -79,6 +79,7 @@ const state = await gameClient.createRoom({
79
79
  - **WebRTC**: Real-time audio/video communication
80
80
  - **Payments**: Subscription and one-time payment support
81
81
  - **AI Streaming**: Real-time AI text generation via WebSocket (Gemini)
82
+ - **Endpoint**: Call your own GPU models on your own PC through one `cb_pk_*` key — ConnectBase forwards the payload as-is (dumb pipe)
82
83
  - **CLI**: Command-line tool for deploying web storage and tunneling local services
83
84
 
84
85
  ## CLI
@@ -127,6 +128,8 @@ npx connectbase deploy ./dist -s <storage-id> -k <public-key>
127
128
  | `--base-url <url>` | `-u` | Custom server URL |
128
129
  | `--timeout <sec>` | `-t` | Tunnel request timeout in seconds (tunnel only) |
129
130
  | `--max-body <MB>` | | Tunnel max body size in MB (tunnel only) |
131
+ | `--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, …)` |
132
+ | `--description <text>` | | Endpoint binding description (only valid with `--label`) |
130
133
  | `--help` | `-h` | Show help |
131
134
  | `--version` | `-v` | Show version |
132
135
 
@@ -164,6 +167,23 @@ Features:
164
167
  - Graceful shutdown with Ctrl+C
165
168
  - No external dependencies (uses Node.js built-in modules)
166
169
 
170
+ #### Auto-register an endpoint binding (`--label`)
171
+
172
+ For workflows where you want the SDK to call your local model by a stable name
173
+ (`cb.endpoint.call("comfyui-main", …)`) instead of a random tunnel URL, pass
174
+ `--label <name>`. The CLI registers the issued `tunnel_id` as an endpoint binding
175
+ on the server, so your SDK only needs the Public Key.
176
+
177
+ ```bash
178
+ # Start ComfyUI on port 8188, expose it as endpoint label "comfyui-main"
179
+ npx connectbase tunnel 8188 --label comfyui-main --description "ComfyUI on my desktop"
180
+ ```
181
+
182
+ Authentication uses your User Secret Key (`cb_sk_*`); the CLI calls the dual-auth
183
+ route `POST /v1/apps/:appID/endpoints/cli`. If the label already exists, the CLI
184
+ warns and keeps the tunnel running — update the binding to the new `tunnel_id`
185
+ from the console if needed.
186
+
167
187
  ### Configuration File
168
188
 
169
189
  The `init` command creates `.connectbaserc` automatically. You can also create it manually:
@@ -791,6 +811,78 @@ await session.stop()
791
811
  | `promptTokens` | `number` | Input prompt tokens |
792
812
  | `duration` | `number` | Generation time in ms |
793
813
 
814
+ ### Endpoint (Local Model Tunnel)
815
+
816
+ `cb.endpoint.*` is a dumb pipe to your own GPU/model server running behind a
817
+ ConnectBase tunnel. ConnectBase doesn't know your model, payload, or response
818
+ shape — it routes a `cb_pk_*` call by label to the registered tunnel and forwards
819
+ the body and headers as-is.
820
+
821
+ **Setup**: run `connectbase tunnel <port> --label <name>` once on the machine
822
+ hosting the model (see [Tunnel](#tunnel)) — that registers the binding. Then any
823
+ client with the app's Public Key can call it.
824
+
825
+ #### `cb.endpoint.call(label, init): Promise<Response>`
826
+
827
+ `fetch()`-compatible signature. Returns the raw `Response` — read the body as
828
+ JSON, text, or stream as needed.
829
+
830
+ ```typescript
831
+ const cb = new ConnectBase({ publicKey: 'cb_pk_...' })
832
+
833
+ // ComfyUI prompt graph
834
+ const res = await cb.endpoint.call('comfyui-main', {
835
+ method: 'POST',
836
+ path: '/prompt',
837
+ headers: { 'Content-Type': 'application/json' },
838
+ body: JSON.stringify({ prompt: { /* ComfyUI node graph */ } }),
839
+ })
840
+ const data = await res.json()
841
+ ```
842
+
843
+ ```typescript
844
+ // Streaming response (SSE / chunked) — vLLM chat completions
845
+ const res = await cb.endpoint.call('vllm-local', {
846
+ method: 'POST',
847
+ path: '/v1/chat/completions',
848
+ headers: { 'Content-Type': 'application/json' },
849
+ body: JSON.stringify({ stream: true, messages: [/* { role, content } */] }),
850
+ })
851
+ if (!res.body) throw new Error('no stream')
852
+ const reader = res.body.getReader()
853
+ while (true) {
854
+ const { done, value } = await reader.read()
855
+ if (done) break
856
+ // value is a Uint8Array — decode and process chunk
857
+ }
858
+ ```
859
+
860
+ ```typescript
861
+ // Cancel an in-flight request
862
+ const ctrl = new AbortController()
863
+ setTimeout(() => ctrl.abort(), 30_000)
864
+ await cb.endpoint.call('hunyuan-laptop', {
865
+ method: 'POST',
866
+ path: '/generate',
867
+ signal: ctrl.signal,
868
+ body: JSON.stringify({ /* model input */ }),
869
+ })
870
+ ```
871
+
872
+ **`EndpointCallInit`**
873
+
874
+ | Field | Type | Required | Description |
875
+ |-------|------|----------|-------------|
876
+ | `path` | `string` | yes | Path on your model server, must start with `/` (e.g. `/prompt`, `/v1/chat/completions`) |
877
+ | `method` | `string` | no (`GET`) | HTTP method |
878
+ | `headers` | `HeadersInit` | no | Extra request headers; `X-Public-Key` is auto-injected unless you set it |
879
+ | `body` | `BodyInit \| null` | no | Request body — `string`, `Blob`, `ArrayBuffer`, `FormData`, or `ReadableStream` |
880
+ | `signal` | `AbortSignal` | no | Abort signal for cancellation |
881
+
882
+ The SDK assembles the URL as `${baseUrl}/v1/proxy/${label}${path}` and forwards
883
+ the request. Because the response is the raw `fetch` `Response`, streaming
884
+ formats (SSE, chunked, NDJSON) work out of the box.
885
+
794
886
  ### Push Notifications
795
887
 
796
888
  ```typescript
package/dist/cli.js CHANGED
@@ -1565,6 +1565,41 @@ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
1565
1565
  success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
1566
1566
  return { appId: createData.app_id, publicKey: createData.public_key };
1567
1567
  }
1568
+ async function registerEndpointBinding(baseUrl, appId, secretKey, tunnelUrl, label, description) {
1569
+ try {
1570
+ const u = new URL(tunnelUrl);
1571
+ const host = u.hostname;
1572
+ const tunnelId = host.replace(/\.tunnel\.connectbase\.world$/, "");
1573
+ if (!tunnelId || tunnelId === host) {
1574
+ log(`${colors.yellow}\u26A0 tunnel_id \uCD94\uCD9C \uC2E4\uD328 (${tunnelUrl}) \u2014 endpoint \uC790\uB3D9 \uB4F1\uB85D skip${colors.reset}`);
1575
+ return;
1576
+ }
1577
+ const apiBase = baseUrl.replace(/\/+$/, "");
1578
+ const res = await fetch(`${apiBase}/v1/apps/${encodeURIComponent(appId)}/endpoints/cli`, {
1579
+ method: "POST",
1580
+ headers: {
1581
+ "Content-Type": "application/json",
1582
+ "Authorization": `Bearer ${secretKey}`
1583
+ },
1584
+ body: JSON.stringify({
1585
+ label,
1586
+ tunnel_id: tunnelId,
1587
+ description: description ?? `CLI tunnel start (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)})`
1588
+ })
1589
+ });
1590
+ if (res.status === 201) {
1591
+ success(`Endpoint "${label}" \uC790\uB3D9 \uB4F1\uB85D \uC644\uB8CC`);
1592
+ log(`${colors.green}\u2192${colors.reset} SDK: ${colors.cyan}cb.endpoint.call("${label}", { path: "/...", method: "POST", body: ... })${colors.reset}`);
1593
+ } else if (res.status === 409) {
1594
+ log(`${colors.yellow}\u26A0 "${label}" \uB77C\uBCA8\uC774 \uC774\uBBF8 \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.${colors.reset} \uC0C8 tunnel_id \uB85C \uAC31\uC2E0\uD558\uB824\uBA74 \uCF58\uC194\uC5D0\uC11C \uC218\uB3D9 PATCH.`);
1595
+ } else {
1596
+ const text = await res.text().catch(() => "");
1597
+ log(`${colors.yellow}\u26A0 endpoint \uB4F1\uB85D \uC2E4\uD328 (status ${res.status}): ${text}${colors.reset}`);
1598
+ }
1599
+ } catch (err) {
1600
+ log(`${colors.yellow}\u26A0 endpoint \uB4F1\uB85D \uC911 \uC5D0\uB7EC: ${err instanceof Error ? err.message : err}${colors.reset}`);
1601
+ }
1602
+ }
1568
1603
  function acquireTunnelLock2(appID, port, force) {
1569
1604
  const result = acquireTunnelLock(appID, port, force, VERSION);
1570
1605
  if (result && result.startsWith("LOCKED:")) {
@@ -1796,6 +1831,16 @@ ${colors.dim}\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C \uC2DC \uB2E4\uC74C \uD5E4\u
1796
1831
  log(`${colors.dim} $ curl -H "X-Proxy-Token: ${proxyToken}" ${tunnelUrl}/${colors.reset}`);
1797
1832
  log(`${colors.dim} $ curl "${tunnelUrl}/?proxy_token=${proxyToken}"${colors.reset}`);
1798
1833
  }
1834
+ if (tunnelOpts?.label && tunnelUrl) {
1835
+ void registerEndpointBinding(
1836
+ config.baseUrl,
1837
+ appId,
1838
+ tunnelKey,
1839
+ tunnelUrl,
1840
+ tunnelOpts.label,
1841
+ tunnelOpts.description
1842
+ );
1843
+ }
1799
1844
  log(`
1800
1845
  ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
1801
1846
  `);
@@ -1968,6 +2013,8 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
1968
2013
  --force \uD130\uB110 lockfile \uBB34\uC2DC (\uC911\uBCF5 \uC2E4\uD589 \uAC15\uC81C, tunnel \uC804\uC6A9)
1969
2014
  --public proxy_token \uAC80\uC99D \uBE44\uD65C\uC131\uD654 \u2014 \uC6F9\uD6C5/\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C\uC6A9 (tunnel \uC804\uC6A9)
1970
2015
  --show-token proxy_token \uACFC curl \uC608\uC2DC\uB97C \uCD9C\uB825 (--public \uC544\uB2CC \uACBD\uC6B0)
2016
+ --label <name> Endpoint binding \uC790\uB3D9 \uB4F1\uB85D (tunnel \uC804\uC6A9) \u2014 SDK \uC758 cb.endpoint.call(label) \uD638\uCD9C \uAC00\uB2A5
2017
+ --description <text> Endpoint binding \uC124\uBA85 (--label \uB3D9\uBC18 \uC2DC\uB9CC)
1971
2018
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
1972
2019
  --check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
1973
2020
  --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
@@ -2148,6 +2195,12 @@ async function main() {
2148
2195
  if (parsed.options.showToken === "true") {
2149
2196
  tunnelOpts.showToken = true;
2150
2197
  }
2198
+ if (parsed.options.label) {
2199
+ tunnelOpts.label = parsed.options.label;
2200
+ }
2201
+ if (parsed.options.description) {
2202
+ tunnelOpts.description = parsed.options.description;
2203
+ }
2151
2204
  await startTunnel(port, config, tunnelOpts);
2152
2205
  } else {
2153
2206
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);