connectbase-client 3.29.0 → 3.31.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 +24 -0
- package/README.md +6 -3
- package/dist/cli.js +35 -10
- package/dist/connect-base.umd.js +4 -4
- package/dist/index.d.mts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +85 -3
- package/dist/index.mjs +85 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.31.0] - 2026-06-05
|
|
7
|
+
|
|
8
|
+
### Added — OAuth 리다이렉트 콜백 **부팅 자동 소비** 안전망
|
|
9
|
+
|
|
10
|
+
소셜 로그인(구글 등) 리다이렉트 후 "로그인했는데 자꾸 로그인 페이지로 되돌아오는" 무한 루프를
|
|
11
|
+
SDK 레벨에서 차단한다. 여러 사용자가 동시에 보고한 회귀의 근본 안전망.
|
|
12
|
+
|
|
13
|
+
- **근본 원인**: 리다이렉트 방식은 콜백 URL(`?access_token=&refresh_token=&member_id=`)에서
|
|
14
|
+
앱이 `cb.oauth.getCallbackResult()` 를 호출해야 토큰이 적재되고 cookie 가 부트스트랩된다.
|
|
15
|
+
그런데 정적 호스팅(웹 스토리지)은 SPA fallback 으로 `/auth/callback` 같은 경로에 홈
|
|
16
|
+
`index.html` 을 서빙하므로, 앱이 콜백 처리 코드를 빠뜨리면 URL 토큰이 **영영 소비되지 않아**
|
|
17
|
+
세션이 확립되지 않고 인증 가드가 다시 로그인으로 되돌린다.
|
|
18
|
+
- **변경**: `new ConnectBase()` 생성자가 토큰-in-URL 리다이렉트 콜백(팝업 제외)을 감지하면
|
|
19
|
+
`oauth.consumeRedirectCallbackOnBoot()` 를 boot-restore promise 로 등록해 **자동으로 토큰을
|
|
20
|
+
적재 + cookie 부트스트랩**한다. 앱이 `getCallbackResult()` 를 호출하지 않아도 세션이 확립된다.
|
|
21
|
+
`prepareHeaders` 가 이 promise 를 await 하므로 첫 `getMe()`(인증 가드 등)가 세션 확립 후 발화.
|
|
22
|
+
- **하위호환·안전성**: 앱이 이후 `getCallbackResult()` 를 호출해도 같은 promise 를 공유해 **이중
|
|
23
|
+
bootstrap/rotation 을 방지**(boot 소비 실패 시에만 직접 적재로 폴백). 팝업(`window.opener`)·
|
|
24
|
+
code-only(`?code=`)·에러(`?error=`) 콜백은 제외 — 기존 동작 유지. `autoRestoreSession: false`
|
|
25
|
+
면 자동 소비도 끈다. token rotation race(2026-05-16) 없음: 사전 re-issue 없이 URL 토큰을 직접
|
|
26
|
+
소비(=getCallbackResult 리다이렉트 경로와 동일)하기 때문.
|
|
27
|
+
|
|
28
|
+
> 이미 배포된 앱도 CDN 번들(`connect-base.min.js`) / npm `latest` 갱신 시 코드 수정 없이 복구됩니다.
|
|
29
|
+
|
|
6
30
|
## [3.29.0] - 2026-06-02
|
|
7
31
|
|
|
8
32
|
### Added — `connectbase tunnel --token <고정값>` proxy_token 핀 (platform-issue 019e8623)
|
package/README.md
CHANGED
|
@@ -1048,9 +1048,12 @@ HTTP response (for example, Discord Interactions requires a `200` with a
|
|
|
1048
1048
|
JSON body within 3 seconds):
|
|
1049
1049
|
|
|
1050
1050
|
```javascript
|
|
1051
|
-
export async function handler(
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1051
|
+
export async function handler(rawBody, ctx) {
|
|
1052
|
+
// The first arg is the raw request body as a UTF-8 string (parse it yourself:
|
|
1053
|
+
// JSON.parse(rawBody) for JSON, new URLSearchParams(rawBody) for form-encoded).
|
|
1054
|
+
// ctx.method / ctx.path / ctx.query / ctx.headers / ctx.rawBody (Buffer) are
|
|
1055
|
+
// populated for webhook invocations. The first arg is the body itself (a
|
|
1056
|
+
// string), not a request object with a `.body` field.
|
|
1054
1057
|
return {
|
|
1055
1058
|
statusCode: 200,
|
|
1056
1059
|
headers: { 'Content-Type': 'application/json' },
|
package/dist/cli.js
CHANGED
|
@@ -1497,6 +1497,8 @@ var UpstreamWsFrameParser = class {
|
|
|
1497
1497
|
constructor(handlers) {
|
|
1498
1498
|
this.buffer = Buffer.alloc(0);
|
|
1499
1499
|
this.fragmentBuffer = null;
|
|
1500
|
+
// accumulated payload across FIN=0 frames
|
|
1501
|
+
this.fragmentOpcode = 2;
|
|
1500
1502
|
this.h = handlers;
|
|
1501
1503
|
}
|
|
1502
1504
|
feed(chunk) {
|
|
@@ -1559,16 +1561,17 @@ var UpstreamWsFrameParser = class {
|
|
|
1559
1561
|
if (fin) {
|
|
1560
1562
|
const out = this.fragmentBuffer;
|
|
1561
1563
|
this.fragmentBuffer = null;
|
|
1562
|
-
this.h.onPayload(out);
|
|
1564
|
+
this.h.onPayload(out, this.fragmentOpcode);
|
|
1563
1565
|
}
|
|
1564
1566
|
break;
|
|
1565
1567
|
case 1:
|
|
1566
1568
|
// TEXT
|
|
1567
1569
|
case 2:
|
|
1568
1570
|
if (fin) {
|
|
1569
|
-
this.h.onPayload(payloadCopy);
|
|
1571
|
+
this.h.onPayload(payloadCopy, opcode);
|
|
1570
1572
|
} else {
|
|
1571
1573
|
this.fragmentBuffer = payloadCopy;
|
|
1574
|
+
this.fragmentOpcode = opcode;
|
|
1572
1575
|
}
|
|
1573
1576
|
break;
|
|
1574
1577
|
case 8:
|
|
@@ -1588,6 +1591,9 @@ var UpstreamWsFrameParser = class {
|
|
|
1588
1591
|
function createUpstreamTextFrame(payload) {
|
|
1589
1592
|
return buildClientFrame(129, payload);
|
|
1590
1593
|
}
|
|
1594
|
+
function createUpstreamBinaryFrame(payload) {
|
|
1595
|
+
return buildClientFrame(130, payload);
|
|
1596
|
+
}
|
|
1591
1597
|
function createUpstreamPongFrame(payload) {
|
|
1592
1598
|
return buildClientFrame(138, payload);
|
|
1593
1599
|
}
|
|
@@ -1857,7 +1863,7 @@ async function startTunnel(port, config, tunnelOpts) {
|
|
|
1857
1863
|
const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
|
|
1858
1864
|
const parsedUrl = new URL(tunnelServerUrl);
|
|
1859
1865
|
const isHttps = parsedUrl.protocol === "https:";
|
|
1860
|
-
let wsPath = `/v2/tunnel/connect?app_id=${encodeURIComponent(appId)}&local_port=${port}`;
|
|
1866
|
+
let wsPath = `/v2/tunnel/connect?app_id=${encodeURIComponent(appId)}&local_port=${port}&ws_opcode=1`;
|
|
1861
1867
|
if (tunnelOpts?.timeout) {
|
|
1862
1868
|
wsPath += `&timeout=${tunnelOpts.timeout}`;
|
|
1863
1869
|
}
|
|
@@ -1874,6 +1880,7 @@ async function startTunnel(port, config, tunnelOpts) {
|
|
|
1874
1880
|
const maxReconnectAttempts = 10;
|
|
1875
1881
|
let shouldReconnect = true;
|
|
1876
1882
|
let socket = null;
|
|
1883
|
+
let negotiatedWsOpcode = false;
|
|
1877
1884
|
let lockReleased = false;
|
|
1878
1885
|
const releaseLock = () => {
|
|
1879
1886
|
if (lockPath && !lockReleased) {
|
|
@@ -2147,9 +2154,13 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
2147
2154
|
});
|
|
2148
2155
|
log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${colors.cyan}WS${colors.reset} ${reqPath} \u2192 101`);
|
|
2149
2156
|
const upstreamParser = new UpstreamWsFrameParser({
|
|
2150
|
-
onPayload: (payload) => {
|
|
2157
|
+
onPayload: (payload, opcode) => {
|
|
2151
2158
|
if (cancelled) return;
|
|
2152
|
-
|
|
2159
|
+
if (negotiatedWsOpcode) {
|
|
2160
|
+
sendBinary(sock, streamId, Buffer.concat([Buffer.from([opcode & 15]), payload]));
|
|
2161
|
+
} else {
|
|
2162
|
+
sendBinary(sock, streamId, payload);
|
|
2163
|
+
}
|
|
2153
2164
|
},
|
|
2154
2165
|
onPing: (payload) => {
|
|
2155
2166
|
if (cancelled || !upstream) return;
|
|
@@ -2205,13 +2216,25 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
2205
2216
|
// Encode the v2 payload as an upstream WS frame BEFORE writing to
|
|
2206
2217
|
// the socket. The prior implementation wrote raw payload to the
|
|
2207
2218
|
// socket which is not a valid WS frame; upstream WS servers (e.g.
|
|
2208
|
-
// ComfyUI) would error out.
|
|
2209
|
-
//
|
|
2210
|
-
//
|
|
2211
|
-
//
|
|
2219
|
+
// ComfyUI) would error out.
|
|
2220
|
+
//
|
|
2221
|
+
// When ws_opcode is negotiated, the chunk is [opcode][payload] — we
|
|
2222
|
+
// re-frame with the client's original TEXT/BINARY opcode so binary
|
|
2223
|
+
// subprotocols (e.g. noVNC's `binary` over websockify) work
|
|
2224
|
+
// bidirectionally (platform-issue 019e86ea). Without negotiation we
|
|
2225
|
+
// fall back to TEXT — the long-standing limitation that silently
|
|
2226
|
+
// dropped client→upstream binary frames.
|
|
2212
2227
|
feedBody: (chunk) => {
|
|
2213
2228
|
if (cancelled || !upstream) return;
|
|
2214
|
-
|
|
2229
|
+
if (negotiatedWsOpcode && chunk.length >= 1) {
|
|
2230
|
+
const opcode = chunk[0] & 15;
|
|
2231
|
+
const payload = chunk.subarray(1);
|
|
2232
|
+
upstream.write(
|
|
2233
|
+
opcode === 2 ? createUpstreamBinaryFrame(payload) : createUpstreamTextFrame(payload)
|
|
2234
|
+
);
|
|
2235
|
+
} else {
|
|
2236
|
+
upstream.write(createUpstreamTextFrame(chunk));
|
|
2237
|
+
}
|
|
2215
2238
|
},
|
|
2216
2239
|
endBody: () => {
|
|
2217
2240
|
},
|
|
@@ -2231,6 +2254,8 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
2231
2254
|
const proxyToken = typeof msg.proxy_token === "string" ? msg.proxy_token : "";
|
|
2232
2255
|
const tunnelUrl = typeof msg.url === "string" ? msg.url : "";
|
|
2233
2256
|
const isPublic = msg.public === true;
|
|
2257
|
+
const readyFeatures = Array.isArray(msg.features) ? msg.features : [];
|
|
2258
|
+
negotiatedWsOpcode = readyFeatures.includes("ws_opcode");
|
|
2234
2259
|
success(`\uD130\uB110 \uD65C\uC131\uD654!`);
|
|
2235
2260
|
log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${tunnelUrl}${colors.reset}`);
|
|
2236
2261
|
log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
|