connectbase-client 3.44.0 → 3.46.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,22 @@
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.46.0] - 2026-07-11
7
+
8
+ ### Added — 크로스 플랫폼 음성 인식(STT) API
9
+
10
+ 패키징된 WebView 앱(특히 iOS WKWebView)에서 `webkitSpeechRecognition` 이 없어 음성 입력이 불가능하던 문제를 해결하기 위해 `cb.native.speech` 를 추가했다.
11
+
12
+ - `cb.native.speech.isAvailable()` — 음성 인식 지원 여부
13
+ - `cb.native.speech.recognize({ lang, interim, continuous, onPartial })` — 음성 인식 시작, 최종 transcript 반환. `onPartial` 로 중간 결과 실시간 수신.
14
+ - `cb.native.speech.stop()` — 진행 중 인식 중지
15
+ - 플랫폼 자동 분기: 웹/데스크톱=Web Speech API, 패키징 모바일 앱=네이티브 브릿지(iOS `SFSpeechRecognizer` / Android `SpeechRecognizer`). 패키징 시 `native_features.speech` 필요.
16
+ - 신규 타입 export: `SpeechRecognizeOptions`, `SpeechResult`.
17
+
18
+ ### Changed
19
+
20
+ - `NativeBridge.push.getToken()` 반환 타입에 `platform`('ios' | 'android') 추가. 패키징 셸이 이제 raw APNS/FCM 디바이스 토큰을 제공하므로 `cb.push.registerDevice({ device_token, platform })` 에 그대로 전달 가능.
21
+
6
22
  ## [3.44.0] - 2026-07-03
7
23
 
8
24
  ### Added — CLI 비대화형 init + SDK 문서 수명주기 정비
package/dist/cli.js CHANGED
@@ -429,7 +429,10 @@ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, ti
429
429
  const presignFiles = largeBinaries.map((f) => ({
430
430
  path: f.path,
431
431
  size: Buffer.from(f.content, "base64").length,
432
- mime_type: guessMimeType(f.path)
432
+ mime_type: guessMimeType(f.path),
433
+ // 신형 서버: hash 제공 시 content-addressed blob 키({id}/blobs/{hash})로 presign 되고,
434
+ // 동일 blob 이 이미 있으면 already_exists 로 업로드를 생략할 수 있다. 구버전 서버는 무시.
435
+ hash: f.hash
433
436
  }));
434
437
  let presigned;
435
438
  try {
@@ -445,22 +448,30 @@ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, ti
445
448
  warn(`\uB300\uC6A9\uB7C9 \uC5D0\uC14B\uC740 \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(${colors.cyan}cb.storage${colors.reset})\uC5D0 \uC62C\uB9AC\uACE0 URL \uB85C \uCC38\uC870\uD558\uAC70\uB098, \uC11C\uBC84 \uC5C5\uB370\uC774\uD2B8 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`);
446
449
  process.exit(1);
447
450
  }
448
- const urlByPath = new Map(presigned.map((p) => [p.path, p.upload_url]));
451
+ const entryByPath = new Map(presigned.map((p) => [p.path, p]));
449
452
  info(`\uB300\uC6A9\uB7C9 \uBC14\uC774\uB108\uB9AC ${largeBinaries.length}\uAC1C\uB97C Object Storage \uB85C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4...`);
450
453
  for (const f of largeBinaries) {
451
- const uploadUrl = urlByPath.get(f.path);
452
- if (!uploadUrl) {
453
- error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
454
+ const entry = entryByPath.get(f.path);
455
+ if (!entry) {
456
+ error(`presign \uC751\uB2F5\uC5D0 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
454
457
  process.exit(1);
455
458
  }
456
459
  const bytes = Buffer.from(f.content, "base64");
457
- try {
458
- await putToPresignedUrl(uploadUrl, bytes, guessMimeType(f.path), timeoutMs);
459
- } catch (err) {
460
- error(`\uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
461
- process.exit(1);
460
+ if (entry.already_exists) {
461
+ success(`\uC5C5\uB85C\uB4DC \uC0DD\uB7B5(\uAE30\uC874 blob \uC7AC\uC0AC\uC6A9): ${f.path} (${formatSize(bytes.length)})`);
462
+ } else {
463
+ if (!entry.upload_url) {
464
+ error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
465
+ process.exit(1);
466
+ }
467
+ try {
468
+ await putToPresignedUrl(entry.upload_url, bytes, guessMimeType(f.path), timeoutMs);
469
+ } catch (err) {
470
+ error(`\uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
471
+ process.exit(1);
472
+ }
473
+ success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC: ${f.path} (${formatSize(bytes.length)})`);
462
474
  }
463
- success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC: ${f.path} (${formatSize(bytes.length)})`);
464
475
  f.pre_uploaded = true;
465
476
  f.size = bytes.length;
466
477
  f.content = "";
@@ -743,8 +754,25 @@ async function fullDeploy(baseStorageUrl, headers, files, envLabel, endpoint, ti
743
754
  }
744
755
  function handleDeployResponse(response, envLabel) {
745
756
  if (response.status >= 200 && response.status < 300) {
746
- success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
747
757
  const data = response.data;
758
+ const dep = data?.deployment ?? null;
759
+ if (dep?.deployment_id) {
760
+ if (dep.promoted) {
761
+ success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC! (release #${dep.seq})`);
762
+ if (dep.production_url) log(`
763
+ ${colors.cyan}Production: ${dep.production_url}${colors.reset}`);
764
+ if (dep.qa_url) log(`${colors.dim}QA: ${dep.qa_url}${colors.reset}
765
+ `);
766
+ } else {
767
+ success(`QA \uD658\uACBD\uC5D0 \uBC30\uD3EC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (release #${dep.seq}) \u2014 production \uC740 \uC544\uC9C1 \uC774\uC804 \uBC84\uC804\uC785\uB2C8\uB2E4`);
768
+ if (dep.qa_url) log(`
769
+ ${colors.yellow}QA URL: ${dep.qa_url}${colors.reset}`);
770
+ log(`${colors.dim}\uAC80\uC99D \uD6C4 \uBC18\uC601: ${colors.cyan}npx connectbase promote${colors.reset}
771
+ `);
772
+ }
773
+ return;
774
+ }
775
+ success(`${envLabel} \uBC30\uD3EC \uC644\uB8CC!`);
748
776
  if (data?.dev_url) {
749
777
  log(`
750
778
  ${colors.yellow}Dev URL: ${data.dev_url}${colors.reset}
@@ -761,6 +789,64 @@ ${colors.cyan}URL: ${data.url}${colors.reset}
761
789
  process.exit(1);
762
790
  }
763
791
  }
792
+ async function promote(config, deploymentId) {
793
+ const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
794
+ const headers = { "X-Public-Key": config.publicKey };
795
+ info(deploymentId ? `release ${deploymentId} \uB97C production \uC73C\uB85C \uC2B9\uACA9\uD569\uB2C8\uB2E4...` : "\uCD5C\uC2E0 \uBC30\uD3EC\uB97C production \uC73C\uB85C \uC2B9\uACA9\uD569\uB2C8\uB2E4...");
796
+ const body = JSON.stringify(deploymentId ? { deployment_id: deploymentId } : {});
797
+ const res = await makeRequest(`${baseStorageUrl}/promote`, "POST", headers, body);
798
+ if (res.status === 404 && !res.data?.error) {
799
+ error("\uC11C\uBC84\uAC00 promote \uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (\uAD6C\uBC84\uC804 \uC11C\uBC84) \u2014 \uC11C\uBC84 \uC5C5\uB370\uC774\uD2B8 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694");
800
+ process.exit(1);
801
+ }
802
+ if (res.status < 200 || res.status >= 300) {
803
+ const data2 = res.data;
804
+ const msg = typeof data2 === "object" && data2 !== null ? data2.error || data2.message || JSON.stringify(data2) : `HTTP ${res.status}`;
805
+ error(`promote \uC2E4\uD328: ${msg}`);
806
+ process.exit(1);
807
+ }
808
+ const data = res.data;
809
+ success(`production \uC2B9\uACA9 \uC644\uB8CC! (release #${data.seq})`);
810
+ if (data.production_url) {
811
+ log(`
812
+ ${colors.cyan}Production: ${data.production_url}${colors.reset}
813
+ `);
814
+ }
815
+ }
816
+ async function listReleases(config) {
817
+ const baseStorageUrl = `${config.baseUrl}/v1/public/storages/webs/${config.storageId}`;
818
+ const headers = { "X-Public-Key": config.publicKey };
819
+ const res = await makeRequest(`${baseStorageUrl}/deployments`, "GET", headers);
820
+ if (res.status < 200 || res.status >= 300) {
821
+ const data2 = res.data;
822
+ const msg = typeof data2 === "object" && data2 !== null ? data2.error || data2.message || JSON.stringify(data2) : `HTTP ${res.status}`;
823
+ error(`\uBC30\uD3EC \uC774\uB825 \uC870\uD68C \uC2E4\uD328: ${msg}`);
824
+ process.exit(1);
825
+ }
826
+ const data = res.data;
827
+ if (!data.deployments || data.deployments.length === 0) {
828
+ info("\uBC30\uD3EC \uC774\uB825\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 deploy \uD558\uC138\uC694.");
829
+ return;
830
+ }
831
+ log(`
832
+ ${colors.cyan}\uBC30\uD3EC \uC774\uB825${colors.reset} (promote_mode: ${data.promote_mode || "auto"}, \uCD1D ${data.total}\uAC1C)
833
+ `);
834
+ for (const d of data.deployments) {
835
+ const markers = [];
836
+ if (d.is_production) markers.push(`${colors.green}production${colors.reset}`);
837
+ if (d.is_qa) markers.push(`${colors.yellow}qa${colors.reset}`);
838
+ const marker = markers.length > 0 ? ` \u2190 ${markers.join(", ")}` : "";
839
+ const statusColor = d.status === "ready" ? colors.green : d.status === "failed" ? colors.red : colors.yellow;
840
+ log(
841
+ ` #${String(d.seq).padEnd(4)} ${statusColor}${d.status.padEnd(9)}${colors.reset} ${String(d.file_count).padStart(4)}\uAC1C ${formatSize(d.total_size).padStart(9)} ${d.created_at} ${colors.dim}${d.id}${colors.reset}${marker}`
842
+ );
843
+ }
844
+ log("");
845
+ if (data.production_url) log(`${colors.cyan}Production: ${data.production_url}${colors.reset}`);
846
+ if (data.qa_url) log(`${colors.yellow}QA: ${data.qa_url}${colors.reset}`);
847
+ log(`${colors.dim}\uB864\uBC31: npx connectbase promote <deployment_id>${colors.reset}
848
+ `);
849
+ }
764
850
  function prompt(question) {
765
851
  const rl = readline.createInterface({
766
852
  input: process.stdin,
@@ -2786,6 +2872,9 @@ ${colors.yellow}\uBA85\uB839\uC5B4:${colors.reset}
2786
2872
  docs SDK \uBB38\uC11C \uB2E4\uC6B4\uB85C\uB4DC/\uC5C5\uB370\uC774\uD2B8 (\uBAA8\uB178\uB808\uD3EC \uC790\uB3D9 \uAC10\uC9C0)
2787
2873
  mcp MCP \uC11C\uBC84 \uC124\uC815 (.mcp.json \uC0DD\uC131/\uC5C5\uB370\uC774\uD2B8, \uBAA8\uB178\uB808\uD3EC \uC790\uB3D9 \uAC10\uC9C0)
2788
2874
  deploy <directory> \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uD30C\uC77C \uBC30\uD3EC (--dev: Dev \uD658\uACBD)
2875
+ promote_mode=manual \uC774\uBA74 QA \uD658\uACBD\uC5D0\uB9CC \uBC30\uD3EC\uB429\uB2C8\uB2E4
2876
+ promote [releaseId] QA \uAC80\uC99D \uD6C4 production \uBC18\uC601 (\uBBF8\uC9C0\uC815: \uCD5C\uC2E0 \uBC30\uD3EC, \uACFC\uAC70 id: \uB864\uBC31)
2877
+ releases \uBC30\uD3EC \uC774\uB825 \uC870\uD68C (release \uBC88\uD638/\uC0C1\uD0DC/production\xB7QA \uD3EC\uC778\uD130)
2789
2878
  tunnel <port> \uB85C\uCEEC \uC11C\uBE44\uC2A4\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C
2790
2879
 
2791
2880
  ${colors.yellow}\uCF54\uB4DC \uBD84\uC11D (MCP \uD1B5\uD574 \uC0AC\uC6A9):${colors.reset}
@@ -2842,7 +2931,12 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
2842
2931
  ${colors.dim}# 4. Prod \uBC30\uD3EC${colors.reset}
2843
2932
  npm run deploy
2844
2933
 
2845
- ${colors.dim}# 4. Dev \uD658\uACBD \uBC30\uD3EC (\uB0B4\uBD80 QA\uC6A9)${colors.reset}
2934
+ ${colors.dim}# 4-1. QA \uC6CC\uD06C\uD50C\uB85C\uC6B0 (\uCF58\uC194\uC5D0\uC11C promote_mode=manual \uC124\uC815 \uC2DC)${colors.reset}
2935
+ npx connectbase deploy ./dist ${colors.dim}# QA \uD658\uACBD\uC5D0\uB9CC \uBC30\uD3EC${colors.reset}
2936
+ npx connectbase releases ${colors.dim}# \uBC30\uD3EC \uC774\uB825 \uD655\uC778${colors.reset}
2937
+ npx connectbase promote ${colors.dim}# QA \uAC80\uC99D \uD6C4 production \uBC18\uC601${colors.reset}
2938
+
2939
+ ${colors.dim}# 4-2. Dev \uD658\uACBD \uBC30\uD3EC (\uB0B4\uBD80 \uD655\uC778\uC6A9)${colors.reset}
2846
2940
  npx connectbase deploy ./dist --dev
2847
2941
 
2848
2942
  ${colors.dim}# 5. \uD130\uB110 (\uAE30\uBCF8)${colors.reset}
@@ -2996,6 +3090,20 @@ async function main() {
2996
3090
  } catch {
2997
3091
  }
2998
3092
  }
3093
+ } else if (parsed.command === "promote" || parsed.command === "releases") {
3094
+ if (!config.publicKey) {
3095
+ 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');
3096
+ process.exit(1);
3097
+ }
3098
+ if (!config.storageId) {
3099
+ error('\uC2A4\uD1A0\uB9AC\uC9C0 ID\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. "npx connectbase init"\uC73C\uB85C \uC124\uC815\uD558\uAC70\uB098 -s \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694');
3100
+ process.exit(1);
3101
+ }
3102
+ if (parsed.command === "promote") {
3103
+ await promote(config, parsed.args[0]);
3104
+ } else {
3105
+ await listReleases(config);
3106
+ }
2999
3107
  } else if (parsed.command === "tunnel") {
3000
3108
  const portStr = parsed.args[0];
3001
3109
  if (!portStr) {