connectbase-client 1.4.1 → 1.5.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 +33 -0
- package/dist/cli.js +99 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
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.5.0] - 2026-04-19
|
|
7
|
+
|
|
8
|
+
터널의 proxy_token UX 정리 — 콘솔 AI Config 경로(95%+ 사용자)는 토큰을 건드리지 않고, curl/웹훅처럼 외부에서 터널 URL 을 직접 호출하는 소수 케이스에만 명시적 플래그로 opt-in 하도록 분리.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`--public` 플래그**: proxy_token 검증을 비활성화한 채 터널을 연다. Stripe/GitHub 등 커스텀 헤더를 못 붙이는 웹훅 수신용. tunnel-server 가 `?public=1` 쿼리를 받아 `Tunnel.Public=true` 로 등록하고 proxy_handler 가 토큰 검증을 skip. 활성화 시 CLI 는 눈에 띄는 노란 경고를 출력 (`cli.ts` handleMessage, `backend/cmd/tunnel-server/app/handler/proxy_handler.go`).
|
|
13
|
+
- **`--show-token` 플래그**: proxy_token 값과 `curl -H "X-Proxy-Token: ..."` / `?proxy_token=` 예시를 CLI 에 출력. curl 로 터널을 직접 때려보고 싶을 때만 사용.
|
|
14
|
+
- **감사 로그·메트릭**: 공개 모드 터널 세션 생성 시 `tunnel_public_opened_total{app_id}` 카운터 증가 + 매 요청마다 remote IP/method/path 가 `Public tunnel access` 로그로 남음.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **터널 기동 시 proxy_token 기본 숨김**: 1.4.1 에서 추가했던 "토큰 + curl 예시 자동 출력"을 제거. 기본 터널 사용자는 콘솔 AI Config 가 서버에서 자동으로 토큰을 resolve 하므로 CLI 에 노출할 필요가 없다. 필요하면 `--show-token` 으로 명시적으로 꺼냄.
|
|
19
|
+
- **`tunnel_ready` 프로토콜 메시지에 `public` 필드 추가**: 서버가 CLI 에 현재 세션의 공개 여부를 내려주어 CLI 가 올바른 안내를 출력하도록 함 (`protocol/message.go`).
|
|
20
|
+
|
|
21
|
+
### Why
|
|
22
|
+
|
|
23
|
+
(1) 1.4.1 의 토큰 자동 출력은 기본 플로우(콘솔 AI Config)에서는 "내가 뭘 해야 하나?" 혼란을 유발했다. 대부분 사용자는 토큰 존재조차 모르는 게 맞다. (2) Stripe/GitHub 처럼 커스텀 헤더를 못 실어 보내는 웹훅 수신처는 `?proxy_token=` 쿼리 번거로움 + 세션 재시작 시 재등록 문제가 있었다. `--public` 으로 opt-in 할 수 있게 해 이 케이스를 깔끔히 분리.
|
|
24
|
+
|
|
25
|
+
## [1.4.2] - 2026-04-18
|
|
26
|
+
|
|
27
|
+
`npx connectbase docs` 명령이 init/deploy/tunnel 과 달리 brower auth 흐름을 거치지 않아, Public Key 가 없는 사용자가 키 발급 경로를 모른 채 prompt 만 보던 UX 문제 해결.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`docs` 가 키 없을 때 자동 발급 흐름을 트리거**: `.connectbaserc.publicKey` → `secretKey` 가 있으면 앱 선택만, 없으면 `browserAuthFlow()` → 앱 선택/생성 → Public Key 신규 발급 → `.connectbaserc` 저장까지 자동 수행. 한 번 실행하면 다음부터는 추가 입력 없이 통과 (`cli.ts` `ensureDocsPublicKey`).
|
|
32
|
+
- **`tunnelAppId` 캐시 공유**: docs 와 tunnel 이 동일한 `.connectbaserc.tunnelAppId` 를 사용하므로 한쪽에서 앱을 선택하면 다른 쪽에서도 재사용된다.
|
|
33
|
+
- **죽은 fallback 정리**: `config.publicKey ?? config.publicKey` (자기 자신 fallback), `!config.publicKey && !config.publicKey` (동일 변수 두 번 검사) 제거.
|
|
34
|
+
|
|
35
|
+
### Internal
|
|
36
|
+
|
|
37
|
+
- `resolveAppForTunnel` → `resolveApp` 으로 일반화. 새 앱 생성 시 백엔드(`POST /v1/public/cli/apps`) 응답에 포함된 `public_key` 를 같이 반환하도록 시그니처 확장. tunnel 호출부는 동작 동일.
|
|
38
|
+
|
|
6
39
|
## [1.4.1] - 2026-04-18
|
|
7
40
|
|
|
8
41
|
`connectbase tunnel` CLI 가 공개 URL 호출에 필요한 **proxy token** 을 표시하지 않아, 사용자가 토큰을 알 방법이 없어 모든 요청이 `401 invalid or missing proxy token` 으로 막히던 문제 해결.
|
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)) {
|
|
@@ -1531,6 +1595,9 @@ async function startTunnel(port, config, tunnelOpts) {
|
|
|
1531
1595
|
if (tunnelOpts?.maxBody) {
|
|
1532
1596
|
wsPath += `&max_body=${tunnelOpts.maxBody}`;
|
|
1533
1597
|
}
|
|
1598
|
+
if (tunnelOpts?.public) {
|
|
1599
|
+
wsPath += `&public=1`;
|
|
1600
|
+
}
|
|
1534
1601
|
let reconnectAttempts = 0;
|
|
1535
1602
|
const maxReconnectAttempts = 10;
|
|
1536
1603
|
let shouldReconnect = true;
|
|
@@ -1664,18 +1731,25 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
1664
1731
|
case "tunnel_ready": {
|
|
1665
1732
|
const proxyToken = typeof msg.proxy_token === "string" ? msg.proxy_token : "";
|
|
1666
1733
|
const tunnelUrl = typeof msg.url === "string" ? msg.url : "";
|
|
1734
|
+
const isPublic = msg.public === true;
|
|
1667
1735
|
success(`\uD130\uB110 \uD65C\uC131\uD654!`);
|
|
1668
1736
|
log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${tunnelUrl}${colors.reset}`);
|
|
1669
1737
|
log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
|
|
1670
|
-
if (proxyToken) {
|
|
1671
|
-
log(`${colors.green}\u2192${colors.reset} \uD1A0\uD070: ${colors.yellow}${proxyToken}${colors.reset}`);
|
|
1672
|
-
}
|
|
1673
1738
|
if (msg.timeout || msg.max_body) {
|
|
1674
1739
|
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}`);
|
|
1675
1740
|
}
|
|
1676
|
-
if (
|
|
1741
|
+
if (isPublic) {
|
|
1742
|
+
log("");
|
|
1743
|
+
log(`${colors.yellow}\u26A0 \uACF5\uAC1C \uBAA8\uB4DC (--public):${colors.reset} proxy_token \uAC80\uC99D\uC774 \uBE44\uD65C\uC131\uD654\uB410\uC2B5\uB2C8\uB2E4.`);
|
|
1744
|
+
log(` ${colors.dim}tunnelID \uB97C \uC544\uB294 \uB204\uAD6C\uB098 \uC774 URL \uB85C \uB85C\uCEEC \uC11C\uBE44\uC2A4\uC5D0 \uC811\uADFC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.${colors.reset}`);
|
|
1745
|
+
log(` ${colors.dim}\uC6F9\uD6C5 \uC218\uC2E0 / \uC678\uBD80 \uC11C\uBE44\uC2A4 \uC5F0\uB3D9 \uBAA9\uC801\uC5D0 \uD55C\uD574 \uC0AC\uC6A9\uD558\uACE0, \uBBFC\uAC10 \uB370\uC774\uD130\uB294 \uCDE8\uAE09\uD558\uC9C0 \uB9C8\uC138\uC694.${colors.reset}`);
|
|
1746
|
+
log("");
|
|
1747
|
+
log(`${colors.dim} $ curl ${tunnelUrl}/${colors.reset}`);
|
|
1748
|
+
} else if (tunnelOpts?.showToken && proxyToken && tunnelUrl) {
|
|
1749
|
+
log("");
|
|
1750
|
+
log(`${colors.green}\u2192${colors.reset} \uD1A0\uD070: ${colors.yellow}${proxyToken}${colors.reset}`);
|
|
1677
1751
|
log(`
|
|
1678
|
-
${colors.dim}\
|
|
1752
|
+
${colors.dim}\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C \uC2DC \uB2E4\uC74C \uD5E4\uB354 \uB610\uB294 \uCFFC\uB9AC\uB85C \uD1A0\uD070\uC744 \uC804\uB2EC\uD558\uC138\uC694:${colors.reset}`);
|
|
1679
1753
|
log(`${colors.dim} $ curl -H "X-Proxy-Token: ${proxyToken}" ${tunnelUrl}/${colors.reset}`);
|
|
1680
1754
|
log(`${colors.dim} $ curl "${tunnelUrl}/?proxy_token=${proxyToken}"${colors.reset}`);
|
|
1681
1755
|
}
|
|
@@ -1849,6 +1923,8 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
|
|
|
1849
1923
|
-t, --timeout <sec> \uD130\uB110 \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (\uCD08, tunnel \uC804\uC6A9)
|
|
1850
1924
|
--max-body <MB> \uD130\uB110 \uCD5C\uB300 \uBC14\uB514 \uD06C\uAE30 (MB, tunnel \uC804\uC6A9)
|
|
1851
1925
|
--force \uD130\uB110 lockfile \uBB34\uC2DC (\uC911\uBCF5 \uC2E4\uD589 \uAC15\uC81C, tunnel \uC804\uC6A9)
|
|
1926
|
+
--public proxy_token \uAC80\uC99D \uBE44\uD65C\uC131\uD654 \u2014 \uC6F9\uD6C5/\uC678\uBD80 \uC9C1\uC811 \uD638\uCD9C\uC6A9 (tunnel \uC804\uC6A9)
|
|
1927
|
+
--show-token proxy_token \uACFC curl \uC608\uC2DC\uB97C \uCD9C\uB825 (--public \uC544\uB2CC \uACBD\uC6B0)
|
|
1852
1928
|
-d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
|
|
1853
1929
|
--check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
|
|
1854
1930
|
--skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
|
|
@@ -1926,6 +2002,10 @@ function parseArgs(args) {
|
|
|
1926
2002
|
result.options.appId = args[++i];
|
|
1927
2003
|
} else if (arg === "--force") {
|
|
1928
2004
|
result.options.force = "true";
|
|
2005
|
+
} else if (arg === "--public") {
|
|
2006
|
+
result.options.public = "true";
|
|
2007
|
+
} else if (arg === "--show-token") {
|
|
2008
|
+
result.options.showToken = "true";
|
|
1929
2009
|
} else if (arg === "-d" || arg === "--dev") {
|
|
1930
2010
|
result.options.dev = "true";
|
|
1931
2011
|
} else if (arg === "--check") {
|
|
@@ -1978,20 +2058,13 @@ async function main() {
|
|
|
1978
2058
|
setupRoot: parsed.options.setupRoot === "true"
|
|
1979
2059
|
});
|
|
1980
2060
|
} else if (parsed.command === "docs") {
|
|
1981
|
-
|
|
1982
|
-
if (!docsPublicKey) {
|
|
1983
|
-
docsPublicKey = await prompt(`${colors.blue}?${colors.reset} Public Key: `);
|
|
1984
|
-
if (!docsPublicKey) {
|
|
1985
|
-
error("Public Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
|
|
1986
|
-
process.exit(1);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
2061
|
+
const docsPublicKey = await ensureDocsPublicKey(config);
|
|
1989
2062
|
await downloadDocs(docsPublicKey);
|
|
1990
2063
|
} else if (parsed.command === "mcp") {
|
|
1991
2064
|
await setupMcp();
|
|
1992
2065
|
} else if (parsed.command === "deploy") {
|
|
1993
2066
|
const directory = parsed.args[0] || fileConfig.deployDir || ".";
|
|
1994
|
-
if (!config.publicKey
|
|
2067
|
+
if (!config.publicKey) {
|
|
1995
2068
|
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');
|
|
1996
2069
|
process.exit(1);
|
|
1997
2070
|
}
|
|
@@ -2027,6 +2100,12 @@ async function main() {
|
|
|
2027
2100
|
if (parsed.options.force === "true") {
|
|
2028
2101
|
tunnelOpts.force = true;
|
|
2029
2102
|
}
|
|
2103
|
+
if (parsed.options.public === "true") {
|
|
2104
|
+
tunnelOpts.public = true;
|
|
2105
|
+
}
|
|
2106
|
+
if (parsed.options.showToken === "true") {
|
|
2107
|
+
tunnelOpts.showToken = true;
|
|
2108
|
+
}
|
|
2030
2109
|
await startTunnel(port, config, tunnelOpts);
|
|
2031
2110
|
} else {
|
|
2032
2111
|
error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);
|