connectbase-client 1.4.0 → 1.4.2
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/dist/cli.js +88 -19
- 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
|
+
## [1.4.2] - 2026-04-18
|
|
7
|
+
|
|
8
|
+
`npx connectbase docs` 명령이 init/deploy/tunnel 과 달리 brower auth 흐름을 거치지 않아, Public Key 가 없는 사용자가 키 발급 경로를 모른 채 prompt 만 보던 UX 문제 해결.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`docs` 가 키 없을 때 자동 발급 흐름을 트리거**: `.connectbaserc.publicKey` → `secretKey` 가 있으면 앱 선택만, 없으면 `browserAuthFlow()` → 앱 선택/생성 → Public Key 신규 발급 → `.connectbaserc` 저장까지 자동 수행. 한 번 실행하면 다음부터는 추가 입력 없이 통과 (`cli.ts` `ensureDocsPublicKey`).
|
|
13
|
+
- **`tunnelAppId` 캐시 공유**: docs 와 tunnel 이 동일한 `.connectbaserc.tunnelAppId` 를 사용하므로 한쪽에서 앱을 선택하면 다른 쪽에서도 재사용된다.
|
|
14
|
+
- **죽은 fallback 정리**: `config.publicKey ?? config.publicKey` (자기 자신 fallback), `!config.publicKey && !config.publicKey` (동일 변수 두 번 검사) 제거.
|
|
15
|
+
|
|
16
|
+
### Internal
|
|
17
|
+
|
|
18
|
+
- `resolveAppForTunnel` → `resolveApp` 으로 일반화. 새 앱 생성 시 백엔드(`POST /v1/public/cli/apps`) 응답에 포함된 `public_key` 를 같이 반환하도록 시그니처 확장. tunnel 호출부는 동작 동일.
|
|
19
|
+
|
|
20
|
+
## [1.4.1] - 2026-04-18
|
|
21
|
+
|
|
22
|
+
`connectbase tunnel` CLI 가 공개 URL 호출에 필요한 **proxy token** 을 표시하지 않아, 사용자가 토큰을 알 방법이 없어 모든 요청이 `401 invalid or missing proxy token` 으로 막히던 문제 해결.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`tunnel_ready` 출력에 proxy token 노출**: 터널 활성화 시 서버가 내려보낸 세션 단위 토큰을 stdout 에 표시합니다 (`cli.ts` `handleMessage`). 이전에는 `tunnel_handler.go:300-308` 가 `proxy_token` 을 보내주지만 CLI 가 무시해서 사용자가 토큰을 확인할 경로가 사실상 없었습니다.
|
|
27
|
+
- **사용 예시 안내**: 공개 URL 호출 시 사용해야 하는 헤더(`X-Proxy-Token`)와 쿼리(`?proxy_token=`) 형식을 `curl` 예시로 함께 출력합니다. 백엔드 `proxy_handler.go:231-239` 가 검증하는 입력은 이 두 가지뿐이며 `Authorization`, `Cookie`, Basic Auth 는 받지 않습니다.
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
- 토큰은 터널 세션 단위로 서버가 새로 생성합니다. 세션 종료(WebSocket disconnect) 시 즉시 무효화되므로 stdout 노출에 따른 추가 위험은 거의 없습니다.
|
|
32
|
+
- 백엔드 / 콘솔 / 문서 측 변경은 없습니다 — 클라이언트 표시만 보강.
|
|
33
|
+
|
|
6
34
|
## [1.4.0] - 2026-04-18
|
|
7
35
|
|
|
8
36
|
`FetchDataResponse` 타입을 실제 서버 wire 포맷에 맞추는 타입 정정.
|
package/dist/cli.js
CHANGED
|
@@ -845,6 +845,70 @@ function detectMonorepo(gitRoot) {
|
|
|
845
845
|
}
|
|
846
846
|
return result;
|
|
847
847
|
}
|
|
848
|
+
async function ensureDocsPublicKey(config) {
|
|
849
|
+
if (config.publicKey) return config.publicKey;
|
|
850
|
+
const rcPath = path2.join(process.cwd(), ".connectbaserc");
|
|
851
|
+
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
852
|
+
const readRc = () => {
|
|
853
|
+
if (!fs2.existsSync(rcPath)) return {};
|
|
854
|
+
try {
|
|
855
|
+
return JSON.parse(fs2.readFileSync(rcPath, "utf-8"));
|
|
856
|
+
} catch {
|
|
857
|
+
return {};
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
const writeRc = (data) => {
|
|
861
|
+
fs2.writeFileSync(rcPath, JSON.stringify(data, null, 2) + "\n");
|
|
862
|
+
addToGitignore(".connectbaserc");
|
|
863
|
+
};
|
|
864
|
+
let secretKey = config.secretKey;
|
|
865
|
+
if (!secretKey) {
|
|
866
|
+
info("Public Key \uBC1C\uAE09\uC744 \uC704\uD574 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4...");
|
|
867
|
+
secretKey = await browserAuthFlow();
|
|
868
|
+
const rc2 = readRc();
|
|
869
|
+
rc2.secretKey = secretKey;
|
|
870
|
+
writeRc(rc2);
|
|
871
|
+
config.secretKey = secretKey;
|
|
872
|
+
}
|
|
873
|
+
const savedAppId = readRc().tunnelAppId || "";
|
|
874
|
+
const resolved = await resolveApp(secretKey, baseUrl, savedAppId);
|
|
875
|
+
let publicKey = resolved.publicKey;
|
|
876
|
+
if (!publicKey) {
|
|
877
|
+
info("Public Key \uBC1C\uAE09 \uC911...");
|
|
878
|
+
const res = await makeRequest(
|
|
879
|
+
`${baseUrl}/v1/public/cli/apps/${resolved.appId}/public-keys`,
|
|
880
|
+
"POST",
|
|
881
|
+
{ "X-Public-Key": secretKey },
|
|
882
|
+
JSON.stringify({ name: "CLI Docs Key" })
|
|
883
|
+
);
|
|
884
|
+
if (res.status !== 201) {
|
|
885
|
+
const data2 = res.data;
|
|
886
|
+
const detail = data2?.error || data2?.message || `HTTP ${res.status}`;
|
|
887
|
+
error(`Public Key \uBC1C\uAE09 \uC2E4\uD328: ${detail}`);
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
const data = res.data;
|
|
891
|
+
if (!data.key) {
|
|
892
|
+
error("Public Key \uBC1C\uAE09 \uC751\uB2F5\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4");
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
publicKey = data.key;
|
|
896
|
+
success("Public Key \uBC1C\uAE09 \uC644\uB8CC");
|
|
897
|
+
}
|
|
898
|
+
const rc = readRc();
|
|
899
|
+
let changed = false;
|
|
900
|
+
if (rc.publicKey !== publicKey) {
|
|
901
|
+
rc.publicKey = publicKey;
|
|
902
|
+
changed = true;
|
|
903
|
+
}
|
|
904
|
+
if (rc.tunnelAppId !== resolved.appId) {
|
|
905
|
+
rc.tunnelAppId = resolved.appId;
|
|
906
|
+
changed = true;
|
|
907
|
+
}
|
|
908
|
+
if (changed) writeRc(rc);
|
|
909
|
+
config.publicKey = publicKey;
|
|
910
|
+
return publicKey;
|
|
911
|
+
}
|
|
848
912
|
async function downloadDocs(publicKey, templates, baseDir) {
|
|
849
913
|
if (!baseDir) {
|
|
850
914
|
baseDir = getProjectRoot();
|
|
@@ -1391,9 +1455,9 @@ function getTunnelServerUrl(baseUrl) {
|
|
|
1391
1455
|
}
|
|
1392
1456
|
return baseUrl.replace(/:\d+/, ":8090");
|
|
1393
1457
|
}
|
|
1394
|
-
async function
|
|
1458
|
+
async function resolveApp(secretKey, baseUrl, appIdOption) {
|
|
1395
1459
|
if (appIdOption) {
|
|
1396
|
-
return appIdOption;
|
|
1460
|
+
return { appId: appIdOption };
|
|
1397
1461
|
}
|
|
1398
1462
|
let apps = [];
|
|
1399
1463
|
try {
|
|
@@ -1418,7 +1482,7 @@ async function resolveAppForTunnel(secretKey, baseUrl, appIdOption) {
|
|
|
1418
1482
|
}
|
|
1419
1483
|
if (apps.length === 1) {
|
|
1420
1484
|
success(`\uC571 \uC790\uB3D9 \uC120\uD0DD: ${apps[0].name}`);
|
|
1421
|
-
return apps[0].id;
|
|
1485
|
+
return { appId: apps[0].id };
|
|
1422
1486
|
}
|
|
1423
1487
|
if (apps.length > 0) {
|
|
1424
1488
|
log(`
|
|
@@ -1434,7 +1498,7 @@ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
|
|
|
1434
1498
|
const num = parseInt(choice, 10);
|
|
1435
1499
|
if (num > 0 && num <= apps.length) {
|
|
1436
1500
|
success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
|
|
1437
|
-
return apps[num - 1].id;
|
|
1501
|
+
return { appId: apps[num - 1].id };
|
|
1438
1502
|
}
|
|
1439
1503
|
const projectName = path2.basename(process.cwd());
|
|
1440
1504
|
const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
|
|
@@ -1456,7 +1520,7 @@ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
|
|
|
1456
1520
|
}
|
|
1457
1521
|
const createData = createRes.data;
|
|
1458
1522
|
success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
|
|
1459
|
-
return createData.app_id;
|
|
1523
|
+
return { appId: createData.app_id, publicKey: createData.public_key };
|
|
1460
1524
|
}
|
|
1461
1525
|
function acquireTunnelLock2(appID, port, force) {
|
|
1462
1526
|
const result = acquireTunnelLock(appID, port, force, VERSION);
|
|
@@ -1505,7 +1569,7 @@ async function startTunnel(port, config, tunnelOpts) {
|
|
|
1505
1569
|
} catch {
|
|
1506
1570
|
}
|
|
1507
1571
|
}
|
|
1508
|
-
const appId = await
|
|
1572
|
+
const { appId } = await resolveApp(tunnelKey, config.baseUrl, savedAppId);
|
|
1509
1573
|
try {
|
|
1510
1574
|
let rcData = {};
|
|
1511
1575
|
if (fs2.existsSync(rcPath)) {
|
|
@@ -1661,17 +1725,29 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
1661
1725
|
}
|
|
1662
1726
|
function handleMessage(msg, sock, localPort) {
|
|
1663
1727
|
switch (msg.type) {
|
|
1664
|
-
case "tunnel_ready":
|
|
1728
|
+
case "tunnel_ready": {
|
|
1729
|
+
const proxyToken = typeof msg.proxy_token === "string" ? msg.proxy_token : "";
|
|
1730
|
+
const tunnelUrl = typeof msg.url === "string" ? msg.url : "";
|
|
1665
1731
|
success(`\uD130\uB110 \uD65C\uC131\uD654!`);
|
|
1666
|
-
log(`${colors.green}\u2192${colors.reset} URL:
|
|
1667
|
-
log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC:
|
|
1732
|
+
log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${tunnelUrl}${colors.reset}`);
|
|
1733
|
+
log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
|
|
1734
|
+
if (proxyToken) {
|
|
1735
|
+
log(`${colors.green}\u2192${colors.reset} \uD1A0\uD070: ${colors.yellow}${proxyToken}${colors.reset}`);
|
|
1736
|
+
}
|
|
1668
1737
|
if (msg.timeout || msg.max_body) {
|
|
1669
|
-
log(`${colors.green}\u2192${colors.reset} \uC124\uC815:
|
|
1738
|
+
log(`${colors.green}\u2192${colors.reset} \uC124\uC815: timeout=${colors.cyan}${msg.timeout}s${colors.reset}, max-body=${colors.cyan}${msg.max_body}MB${colors.reset}`);
|
|
1739
|
+
}
|
|
1740
|
+
if (proxyToken && tunnelUrl) {
|
|
1741
|
+
log(`
|
|
1742
|
+
${colors.dim}\uACF5\uAC1C URL \uD638\uCD9C \uC2DC \uB2E4\uC74C \uD5E4\uB354 \uB610\uB294 \uCFFC\uB9AC\uB85C \uD1A0\uD070\uC744 \uC804\uB2EC\uD558\uC138\uC694:${colors.reset}`);
|
|
1743
|
+
log(`${colors.dim} $ curl -H "X-Proxy-Token: ${proxyToken}" ${tunnelUrl}/${colors.reset}`);
|
|
1744
|
+
log(`${colors.dim} $ curl "${tunnelUrl}/?proxy_token=${proxyToken}"${colors.reset}`);
|
|
1670
1745
|
}
|
|
1671
1746
|
log(`
|
|
1672
1747
|
${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
|
|
1673
1748
|
`);
|
|
1674
1749
|
break;
|
|
1750
|
+
}
|
|
1675
1751
|
case "http_request":
|
|
1676
1752
|
forwardRequest(msg, sock, localPort);
|
|
1677
1753
|
break;
|
|
@@ -1966,20 +2042,13 @@ async function main() {
|
|
|
1966
2042
|
setupRoot: parsed.options.setupRoot === "true"
|
|
1967
2043
|
});
|
|
1968
2044
|
} else if (parsed.command === "docs") {
|
|
1969
|
-
|
|
1970
|
-
if (!docsPublicKey) {
|
|
1971
|
-
docsPublicKey = await prompt(`${colors.blue}?${colors.reset} Public Key: `);
|
|
1972
|
-
if (!docsPublicKey) {
|
|
1973
|
-
error("Public Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
|
|
1974
|
-
process.exit(1);
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
2045
|
+
const docsPublicKey = await ensureDocsPublicKey(config);
|
|
1977
2046
|
await downloadDocs(docsPublicKey);
|
|
1978
2047
|
} else if (parsed.command === "mcp") {
|
|
1979
2048
|
await setupMcp();
|
|
1980
2049
|
} else if (parsed.command === "deploy") {
|
|
1981
2050
|
const directory = parsed.args[0] || fileConfig.deployDir || ".";
|
|
1982
|
-
if (!config.publicKey
|
|
2051
|
+
if (!config.publicKey) {
|
|
1983
2052
|
error('Public Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. "npx connectbase init"\uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 -k \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694');
|
|
1984
2053
|
process.exit(1);
|
|
1985
2054
|
}
|