connectbase-client 3.0.0 → 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,43 @@
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
+
34
+ ## [3.0.1] - 2026-04-28
35
+
36
+ ### Fixed
37
+
38
+ - `npx connectbase docs` 가 인증 없이 곧장 문서를 받도록 수정. 백엔드 `/v1/storages/webs/claude-md`
39
+ 는 public 라우트인데 CLI 가 불필요하게 브라우저 인증 → 앱 선택 → Public Key 발급을 강제하던
40
+ 흐름을 제거. 캐시된 publicKey 가 있으면 문서에 박아주고, 없으면 백엔드가 placeholder
41
+ (`YOUR_PUBLIC_KEY_HERE`) 로 대체해 그대로 다운로드.
42
+
6
43
  ## [3.0.0] - 2026-04-28 — BREAKING
7
44
 
8
45
  게임 서버 mechanism-only 재설계. ConnectBase 가 박아두던 게임 룰 (파티/로비/랭킹/매치메이킹/
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
@@ -952,70 +952,6 @@ function detectMonorepo(gitRoot) {
952
952
  }
953
953
  return result;
954
954
  }
955
- async function ensureDocsPublicKey(config) {
956
- if (config.publicKey) return config.publicKey;
957
- const rcPath = path2.join(process.cwd(), ".connectbaserc");
958
- const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
959
- const readRc = () => {
960
- if (!fs2.existsSync(rcPath)) return {};
961
- try {
962
- return JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
963
- } catch {
964
- return {};
965
- }
966
- };
967
- const writeRc = (data) => {
968
- fs2.writeFileSync(rcPath, JSON.stringify(data, null, 2) + "\n");
969
- addToGitignore(".connectbaserc");
970
- };
971
- let secretKey = config.secretKey;
972
- if (!secretKey) {
973
- info("Public Key \uBC1C\uAE09\uC744 \uC704\uD574 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4...");
974
- secretKey = await browserAuthFlow();
975
- const rc2 = readRc();
976
- rc2.secretKey = secretKey;
977
- writeRc(rc2);
978
- config.secretKey = secretKey;
979
- }
980
- const savedAppId = readRc().tunnelAppId || "";
981
- const resolved = await resolveApp(secretKey, baseUrl, savedAppId);
982
- let publicKey = resolved.publicKey;
983
- if (!publicKey) {
984
- info("Public Key \uBC1C\uAE09 \uC911...");
985
- const res = await makeRequest(
986
- `${baseUrl}/v1/public/cli/apps/${resolved.appId}/public-keys`,
987
- "POST",
988
- { "X-Public-Key": secretKey },
989
- JSON.stringify({ name: "CLI Docs Key" })
990
- );
991
- if (res.status !== 201) {
992
- const data2 = res.data;
993
- const detail = data2?.error || data2?.message || `HTTP ${res.status}`;
994
- error(`Public Key \uBC1C\uAE09 \uC2E4\uD328: ${detail}`);
995
- process.exit(1);
996
- }
997
- const data = res.data;
998
- if (!data.key) {
999
- error("Public Key \uBC1C\uAE09 \uC751\uB2F5\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4");
1000
- process.exit(1);
1001
- }
1002
- publicKey = data.key;
1003
- success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
1004
- }
1005
- const rc = readRc();
1006
- let changed = false;
1007
- if (rc.publicKey !== publicKey) {
1008
- rc.publicKey = publicKey;
1009
- changed = true;
1010
- }
1011
- if (rc.tunnelAppId !== resolved.appId) {
1012
- rc.tunnelAppId = resolved.appId;
1013
- changed = true;
1014
- }
1015
- if (changed) writeRc(rc);
1016
- config.publicKey = publicKey;
1017
- return publicKey;
1018
- }
1019
955
  async function downloadDocs(publicKey, templates, baseDir) {
1020
956
  if (!baseDir) {
1021
957
  baseDir = getProjectRoot();
@@ -1629,6 +1565,41 @@ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
1629
1565
  success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
1630
1566
  return { appId: createData.app_id, publicKey: createData.public_key };
1631
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
+ }
1632
1603
  function acquireTunnelLock2(appID, port, force) {
1633
1604
  const result = acquireTunnelLock(appID, port, force, VERSION);
1634
1605
  if (result && result.startsWith("LOCKED:")) {
@@ -1860,6 +1831,16 @@ ${colors.dim}\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C \uC2DC \uB2E4\uC74C \uD5E4\u
1860
1831
  log(`${colors.dim} $ curl -H "X-Proxy-Token: ${proxyToken}" ${tunnelUrl}/${colors.reset}`);
1861
1832
  log(`${colors.dim} $ curl "${tunnelUrl}/?proxy_token=${proxyToken}"${colors.reset}`);
1862
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
+ }
1863
1844
  log(`
1864
1845
  ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
1865
1846
  `);
@@ -2032,6 +2013,8 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
2032
2013
  --force \uD130\uB110 lockfile \uBB34\uC2DC (\uC911\uBCF5 \uC2E4\uD589 \uAC15\uC81C, tunnel \uC804\uC6A9)
2033
2014
  --public proxy_token \uAC80\uC99D \uBE44\uD65C\uC131\uD654 \u2014 \uC6F9\uD6C5/\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C\uC6A9 (tunnel \uC804\uC6A9)
2034
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)
2035
2018
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
2036
2019
  --check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
2037
2020
  --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
@@ -2165,8 +2148,7 @@ async function main() {
2165
2148
  setupRoot: parsed.options.setupRoot === "true"
2166
2149
  });
2167
2150
  } else if (parsed.command === "docs") {
2168
- const docsPublicKey = await ensureDocsPublicKey(config);
2169
- await downloadDocs(docsPublicKey);
2151
+ await downloadDocs(config.publicKey || "");
2170
2152
  } else if (parsed.command === "mcp") {
2171
2153
  await setupMcp();
2172
2154
  } else if (parsed.command === "deploy") {
@@ -2213,6 +2195,12 @@ async function main() {
2213
2195
  if (parsed.options.showToken === "true") {
2214
2196
  tunnelOpts.showToken = true;
2215
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
+ }
2216
2204
  await startTunnel(port, config, tunnelOpts);
2217
2205
  } else {
2218
2206
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);