connectbase-client 1.4.2 → 1.6.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,43 @@
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.6.0] - 2026-04-19
7
+
8
+ 웹 방문 추적에 로그인 회원을 자동 연결 — 콘솔의 **앱 멤버 > 활동기록 > 웹방문기록** 탭이 이제 실제로 채워진다. 이전까지는 SDK 가 배치 이벤트를 `visitor_uid` 만 실어 보내 `web_visitors.app_member_id` 가 전부 NULL 로 남아 있었다.
9
+
10
+ ### Added
11
+
12
+ - **`analytics.setMemberId(id | null)`**: 로그인/로그아웃 시점에 방문자 트래커에 회원 ID 를 전달한다. 이후 모든 페이지뷰/이벤트 배치 (`/v1/public/storages/web/{id}/visitors/batch`) 요청에 `app_member_id` 필드가 포함되어 서버가 게스트 방문자를 회원과 자동 연결한다.
13
+ - **`analytics.getMemberId()`**: 현재 트래커에 설정된 회원 ID 조회. 로그인 상태 확인용.
14
+
15
+ ### Changed
16
+
17
+ - **`auth.signUpMember/signInMember/signInAsGuestMember` 가 내부적으로 `analytics.setMemberId()` 호출**: 기존 `window.__cbSetMember` 전역 콜백만 호출하던 경로가 실제로 배치 큐에 회원 ID 를 적용하도록 연결됨. `auth.signOut()` 시에는 `setMemberId(null)` 로 익명 상태 복귀. `ConnectBase` 생성자에서 `auth._attachAnalytics(analytics)` 로 자동 연결.
18
+ - **`analytics.identify(memberId)` 가 `setMemberId` 로 단순화**: 동작 동일하나 내부 구현이 새 경로로 통합. 기존 호출부는 그대로 동작.
19
+
20
+ ### Why
21
+
22
+ 기존 구현은 `auth.ts` 가 `window.__cbSetMember(memberId)` 를 호출하면 "끝" 이었는데, `AnalyticsAPI` 에는 이 전역 함수를 받을 경로도 없고 로그인 후 쌓이는 배치 이벤트에도 회원 ID 가 첨부되지 않았다. 그 결과 `web_visitors.app_member_id` 가 항상 NULL 이라 앱 멤버 페이지의 방문기록 탭이 텅 비어 있었다. 이제 로그인이 일어나면 그 시점부터의 모든 이벤트 배치에 `app_member_id` 가 자동으로 따라간다.
23
+
24
+ ## [1.5.0] - 2026-04-19
25
+
26
+ 터널의 proxy_token UX 정리 — 콘솔 AI Config 경로(95%+ 사용자)는 토큰을 건드리지 않고, curl/웹훅처럼 외부에서 터널 URL 을 직접 호출하는 소수 케이스에만 명시적 플래그로 opt-in 하도록 분리.
27
+
28
+ ### Added
29
+
30
+ - **`--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`).
31
+ - **`--show-token` 플래그**: proxy_token 값과 `curl -H "X-Proxy-Token: ..."` / `?proxy_token=` 예시를 CLI 에 출력. curl 로 터널을 직접 때려보고 싶을 때만 사용.
32
+ - **감사 로그·메트릭**: 공개 모드 터널 세션 생성 시 `tunnel_public_opened_total{app_id}` 카운터 증가 + 매 요청마다 remote IP/method/path 가 `Public tunnel access` 로그로 남음.
33
+
34
+ ### Changed
35
+
36
+ - **터널 기동 시 proxy_token 기본 숨김**: 1.4.1 에서 추가했던 "토큰 + curl 예시 자동 출력"을 제거. 기본 터널 사용자는 콘솔 AI Config 가 서버에서 자동으로 토큰을 resolve 하므로 CLI 에 노출할 필요가 없다. 필요하면 `--show-token` 으로 명시적으로 꺼냄.
37
+ - **`tunnel_ready` 프로토콜 메시지에 `public` 필드 추가**: 서버가 CLI 에 현재 세션의 공개 여부를 내려주어 CLI 가 올바른 안내를 출력하도록 함 (`protocol/message.go`).
38
+
39
+ ### Why
40
+
41
+ (1) 1.4.1 의 토큰 자동 출력은 기본 플로우(콘솔 AI Config)에서는 "내가 뭘 해야 하나?" 혼란을 유발했다. 대부분 사용자는 토큰 존재조차 모르는 게 맞다. (2) Stripe/GitHub 처럼 커스텀 헤더를 못 실어 보내는 웹훅 수신처는 `?proxy_token=` 쿼리 번거로움 + 세션 재시작 시 재등록 문제가 있었다. `--public` 으로 opt-in 할 수 있게 해 이 케이스를 깔끔히 분리.
42
+
6
43
  ## [1.4.2] - 2026-04-18
7
44
 
8
45
  `npx connectbase docs` 명령이 init/deploy/tunnel 과 달리 brower auth 흐름을 거치지 않아, Public Key 가 없는 사용자가 키 발급 경로를 모른 채 prompt 만 보던 UX 문제 해결.
package/dist/cli.js CHANGED
@@ -1595,6 +1595,9 @@ async function startTunnel(port, config, tunnelOpts) {
1595
1595
  if (tunnelOpts?.maxBody) {
1596
1596
  wsPath += `&max_body=${tunnelOpts.maxBody}`;
1597
1597
  }
1598
+ if (tunnelOpts?.public) {
1599
+ wsPath += `&public=1`;
1600
+ }
1598
1601
  let reconnectAttempts = 0;
1599
1602
  const maxReconnectAttempts = 10;
1600
1603
  let shouldReconnect = true;
@@ -1728,18 +1731,25 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
1728
1731
  case "tunnel_ready": {
1729
1732
  const proxyToken = typeof msg.proxy_token === "string" ? msg.proxy_token : "";
1730
1733
  const tunnelUrl = typeof msg.url === "string" ? msg.url : "";
1734
+ const isPublic = msg.public === true;
1731
1735
  success(`\uD130\uB110 \uD65C\uC131\uD654!`);
1732
1736
  log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${tunnelUrl}${colors.reset}`);
1733
1737
  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
- }
1737
1738
  if (msg.timeout || msg.max_body) {
1738
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}`);
1739
1740
  }
1740
- if (proxyToken && tunnelUrl) {
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}`);
1741
1751
  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}`);
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}`);
1743
1753
  log(`${colors.dim} $ curl -H "X-Proxy-Token: ${proxyToken}" ${tunnelUrl}/${colors.reset}`);
1744
1754
  log(`${colors.dim} $ curl "${tunnelUrl}/?proxy_token=${proxyToken}"${colors.reset}`);
1745
1755
  }
@@ -1913,6 +1923,8 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
1913
1923
  -t, --timeout <sec> \uD130\uB110 \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (\uCD08, tunnel \uC804\uC6A9)
1914
1924
  --max-body <MB> \uD130\uB110 \uCD5C\uB300 \uBC14\uB514 \uD06C\uAE30 (MB, tunnel \uC804\uC6A9)
1915
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)
1916
1928
  -d, --dev Dev \uD658\uACBD\uC5D0 \uBC30\uD3EC (deploy \uC804\uC6A9)
1917
1929
  --check \uBC84\uC804\uB9CC \uD655\uC778 (update \uC804\uC6A9)
1918
1930
  --skip-docs \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8 \uAC74\uB108\uB6F0\uAE30 (update \uC804\uC6A9)
@@ -1990,6 +2002,10 @@ function parseArgs(args) {
1990
2002
  result.options.appId = args[++i];
1991
2003
  } else if (arg === "--force") {
1992
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";
1993
2009
  } else if (arg === "-d" || arg === "--dev") {
1994
2010
  result.options.dev = "true";
1995
2011
  } else if (arg === "--check") {
@@ -2084,6 +2100,12 @@ async function main() {
2084
2100
  if (parsed.options.force === "true") {
2085
2101
  tunnelOpts.force = true;
2086
2102
  }
2103
+ if (parsed.options.public === "true") {
2104
+ tunnelOpts.public = true;
2105
+ }
2106
+ if (parsed.options.showToken === "true") {
2107
+ tunnelOpts.showToken = true;
2108
+ }
2087
2109
  await startTunnel(port, config, tunnelOpts);
2088
2110
  } else {
2089
2111
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);