connectbase-client 3.0.1 → 3.2.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,63 @@
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.2.0] - 2026-04-29
7
+
8
+ ### Added — Endpoint API 헬퍼 (`pollUntil` / `url`)
9
+
10
+ ComfyUI × 웹스토리지 같은 e2e 통합 패턴 (작업 제출 → 폴링 → 결과 저장) 을
11
+ 단일 진입점으로 묶기 위한 헬퍼 2개. `cb.endpoint.call()` 만으로도 가능했지만
12
+ 사용자가 매번 직접 작성하던 보일러플레이트를 SDK 가 흡수.
13
+
14
+ - **`cb.endpoint.pollUntil<T>(label, init, predicate, opts)`** — long-poll 한 줄
15
+ 처리. ComfyUI `/history/{id}`, A1111 `/sdapi/v1/progress`, 자체 큐 API 처럼
16
+ "작업 제출 → 폴링" 패턴 전용. `predicate` 가 값을 반환할 때까지 반복 호출,
17
+ HTTP 5xx/네트워크 오류는 재시도, 4xx 는 즉시 reject, `AbortSignal`/`timeoutMs`
18
+ 지원. `parse: "json" | "text" | "none"` 으로 본문 파싱 방식 선택.
19
+ - **`cb.endpoint.url(label, path)`** — 라벨 + path 의 최종 호출 URL 을 조립해서
20
+ 반환. WebSocket / `<img src>` / `new Image()` 같이 SDK 의 `call()` 이 아닌
21
+ 직접 호출이 필요할 때. `X-Public-Key` 는 자동 주입되지 않으므로 모델 서버가
22
+ 자체 토큰으로 인증을 별도 처리해야 함 (ConnectBase 는 dumb pipe).
23
+ - 신규 export: `PollUntilOptions` 타입.
24
+
25
+ ### Docs
26
+
27
+ - `examples/ai-image-generator/` — UMD CDN 한 줄 + 빌드 도구 0 으로 동작하는
28
+ ComfyUI × 웹스토리지 스타터. SDK 로딩 실패 감지, pre-flight 키 검증, 모든
29
+ KSampler seed 랜덤화, cache-bust 워크플로우 fetch, AbortController 일괄 취소,
30
+ step indicator, localStorage 갤러리, ⌘/Ctrl+Enter 단축키 등 11가지
31
+ 베스트프랙티스 채택.
32
+ - `docs/integration/comfyui-web-storage.md` — e2e 통합 가이드 + "왜 이 구조가
33
+ 정답인가" / "안티패턴 7가지".
34
+
35
+ ## [3.1.0] - 2026-04-29
36
+
37
+ ### Added — Endpoint API (로컬 모델 터널 dumb pipe)
38
+
39
+ 사용자 PC GPU 모델 (ComfyUI / A1111 / Hunyuan3D / vLLM / 자체 FastAPI 등) 을
40
+ `cb_pk_*` 한 키로 호출하는 새 모듈. ConnectBase 는 모델·API·워크플로우를
41
+ 알지 않고, 라벨 → tunnel 매핑만 들고 페이로드/응답을 그대로 통과시킵니다.
42
+
43
+ - **`cb.endpoint.call(label, init)`** — fetch() 시그니처 호환 (path, method,
44
+ headers, body, signal). URL 은 `${baseUrl}/v1/proxy/${label}${path}` 로
45
+ 자동 조립, `X-Public-Key` 헤더 자동 주입. SSE / chunked 스트리밍은
46
+ `res.body.getReader()` 로 그대로 읽기.
47
+ - 신규 export: `EndpointAPI`, `EndpointCallInit`.
48
+
49
+ ### Added — CLI
50
+
51
+ - **`connectbase tunnel <port> --label <name>`** — tunnel 발급 후 endpoint
52
+ binding 을 자동 등록. SDK 사용자가 즉시 `cb.endpoint.call("<label>", { ... })`
53
+ 로 호출 가능. 인증은 User Secret Key (`cb_sk_*`) — dual-auth 라우트
54
+ `POST /v1/apps/:appID/endpoints/cli`.
55
+ - **`--description <text>`** — endpoint binding 의 설명 (`--label` 동반 시만).
56
+ - 이미 등록된 라벨이면 경고 후 진행 (다른 tunnel_id 로 갱신은 콘솔에서 PATCH).
57
+
58
+ ### Fixed
59
+
60
+ - JSDoc 안 중첩 블록주석으로 빌드가 깨지던 회귀 수정 (`*/` 가 주석을 조기
61
+ 종료시키던 케이스).
62
+
6
63
  ## [3.0.1] - 2026-04-28
7
64
 
8
65
  ### 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,143 @@ 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
+
886
+ #### `cb.endpoint.pollUntil<T>(label, init, predicate, opts?): Promise<T>`
887
+
888
+ One-line "submit job → poll for result" pattern. Repeatedly calls the same
889
+ endpoint until `predicate` returns a value. Designed for ComfyUI `/history/{id}`,
890
+ A1111 `/sdapi/v1/progress`, or any custom queue API.
891
+
892
+ Behavior:
893
+ - Calls `cb.endpoint.call(label, init)` and passes the parsed body to `predicate`
894
+ - Returns `undefined` from `predicate` → wait `intervalMs` and retry
895
+ - Returns a value from `predicate` → resolve immediately with that value
896
+ - HTTP `5xx` / network error → retry. HTTP `4xx` → reject (job-level error)
897
+ - `timeoutMs` exceeded or `signal` aborted → reject
898
+
899
+ ```typescript
900
+ type Hist = Record<
901
+ string,
902
+ { outputs: Record<string, { images?: { filename: string }[] }> }
903
+ >
904
+
905
+ const filename = await cb.endpoint.pollUntil<string>(
906
+ 'comfyui-main',
907
+ { path: `/history/${promptId}` },
908
+ (data: Hist) => {
909
+ const entry = data[promptId]
910
+ if (!entry) return undefined // still queued
911
+ for (const out of Object.values(entry.outputs)) {
912
+ const img = out.images?.[0]
913
+ if (img) return img.filename
914
+ }
915
+ return undefined
916
+ },
917
+ { intervalMs: 1000, timeoutMs: 5 * 60_000 },
918
+ )
919
+ ```
920
+
921
+ **`PollUntilOptions`**
922
+
923
+ | Field | Type | Default | Description |
924
+ |-------|------|---------|-------------|
925
+ | `intervalMs` | `number` | `1500` | Poll interval in ms |
926
+ | `timeoutMs` | `number` | `300000` (5 min) | Total timeout in ms — reject if exceeded |
927
+ | `parse` | `'json' \| 'text' \| 'none'` | `'json'` | Body parser. `'json'` falls back to `undefined` on parse error |
928
+ | `signal` | `AbortSignal` | — | External cancel signal — reject immediately on abort |
929
+
930
+ #### `cb.endpoint.url(label, path): string`
931
+
932
+ Returns the assembled call URL (`${baseUrl}/v1/proxy/${label}${path}`) for cases
933
+ where the SDK's `call()` doesn't fit — `<img src>`, `new Image()`, native
934
+ `WebSocket`, etc.
935
+
936
+ ⚠️ Direct calls do **not** get automatic `X-Public-Key` injection. If your model
937
+ server requires authentication, your model server must validate a token of its
938
+ own (ConnectBase is a dumb pipe, not an auth gate).
939
+
940
+ ```typescript
941
+ // Render a ComfyUI output image directly into <img>
942
+ img.src =
943
+ cb.endpoint.url('comfyui-main', `/view?filename=${encodeURIComponent(name)}`)
944
+ ```
945
+
946
+ ```typescript
947
+ // Native WebSocket to a tunneled service
948
+ const ws = new WebSocket(cb.endpoint.url('my-ws-server', '/socket'))
949
+ ```
950
+
794
951
  ### Push Notifications
795
952
 
796
953
  ```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}`);