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 +28 -0
- package/README.md +92 -0
- package/dist/cli.js +53 -0
- package/dist/connect-base.umd.js +3 -3
- package/dist/index.d.mts +104 -1
- package/dist/index.d.ts +104 -1
- package/dist/index.js +52 -0
- package/dist/index.mjs +51 -0
- package/package.json +1 -1
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}`);
|