connectbase-client 3.41.0 → 3.43.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +30 -6
  2. package/dist/cli.js +214 -4
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,20 +3,44 @@
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.43.0] - 2026-07-02
7
+
8
+ ### Added — `--dev` 배포도 대용량 바이너리 지원
9
+
10
+ dev 배포(`connectbase deploy --dev`)는 DB 를 거치지 않고 파일을 dev Object Storage 경로에 직접
11
+ 올리므로 6 MiB(TiDB 엔트리) 한도가 애초에 없다. 3.41.0~3.42.0 은 dev 에서도 fail-fast 로 대용량을
12
+ 막았는데(과도한 제한), 이를 풀고 prod 와 동일하게 처리한다.
13
+
14
+ - 대용량 **바이너리**는 presign(`dev:true`)으로 dev 서빙 경로(`{id}/dev{path}`)에 직접 업로드하고 바디에서 제외(효율).
15
+ - 대응 core-server 가 없으면(구버전) 오프로드 없이 전량 base64 바디로 전송 — dev 는 DB 한도가 없어 안전.
16
+ - dev 는 이제 대용량 파일에 대해 fail-fast 하지 않는다.
17
+
18
+ ## [3.42.0] - 2026-07-02
19
+
20
+ ### Added — 웹 배포 대용량 바이너리 Object Storage 직접 업로드 (플랫폼 이슈 019f221b)
21
+
22
+ 3.41.0 의 fail-fast 를 이어, 한도를 넘는 **바이너리**를 배포할 수 있게 했다. 파일당 6 MiB(TiDB
23
+ 단일 엔트리 한도)를 넘는 바이너리(3D 모델·큰 동영상/폰트 등)를 배포하지 못하던 문제 해결.
24
+
25
+ - **대용량 바이너리 직접 업로드**: 한도를 넘는 바이너리는 presigned URL 로 **Object Storage 의 서빙
26
+ 경로에 직접** 업로드하고, 배포 요청에는 `pre_uploaded` 메타데이터만 전송한다. web-proxy 가 서빙하는
27
+ 바로 그 경로에 안착하므로 동일 origin·참조 재작성/CORS 불필요. (대응 core-server 필요. 미지원 서버면
28
+ 3.41.0 과 동일하게 fail-fast.)
29
+ - **fail-fast 유지**: 오프로드 불가한 파일(대용량 **텍스트**, 또는 서버 미지원)은 업로드 전에 즉시 중단.
30
+ - **3D 에셋 확장자 추가**: `.glb`/`.bin`(바이너리), `.gltf`(JSON)을 허용 확장자에 추가 +
31
+ 정확한 MIME(`model/gltf-binary` 등). 이제 3D 모델을 확장자 위장 없이 번들에 그대로 둘 수 있다.
32
+
6
33
  ## [3.41.0] - 2026-07-02
7
34
 
8
35
  ### Added — 웹 배포 파일당 크기 한도 사전 검증 (fail-fast)
9
36
 
10
37
  `connectbase deploy` 가 파일을 수집한 직후, 서버로 업로드하기 **전에** 각 파일이 웹 스토리지
11
38
  파일당 크기 한도를 넘는지 검사한다(플랫폼 이슈 019f221b). 기존에는 전량 수집·업로드를 마친
12
- 뒤에야 서버에서 원시 DB 오류(`Error 8025 (HY000): entry too large`)로 실패해 원인 파악이
13
- 어려웠다.
39
+ 뒤에야 서버에서 원시 DB 오류(`Error 8025 (HY000): entry too large`)로 실패해 원인 파악이 어려웠다.
14
40
 
15
- - 초과 파일이 있으면 **어떤 파일이 얼마나 큰지**(바이너리는 원본→base64 크기까지) 나열하고,
16
- 파일 스토리지(`cb.storage`) 사용을 안내한 뒤 업로드 없이 즉시 종료한다.
41
+ - 초과 파일이 있으면 어떤 파일이 얼마나 큰지(바이너리는 원본→base64 크기까지) 나열하고 즉시 종료한다.
17
42
  - 검사 기준: 파일 내용은 DB(TiDB Text 컬럼)에 저장되며 단일 엔트리 한도 6 MiB 를 넘을 수 없다.
18
- 바이너리는 base64(약 1.33x)로 저장되므로 원본 기준 실질 상한은 약 4.5 MB.
19
- - 텍스트 파일은 UTF-8 바이트 기준으로 정확히 계산(멀티바이트 문자 포함).
43
+ 바이너리는 base64(약 1.33x)로 저장되므로 원본 기준 실질 상한 약 4.5 MB. 텍스트는 UTF-8 바이트로 정확히 계산.
20
44
 
21
45
  ## [3.40.0] - 2026-06-28
22
46
 
package/dist/cli.js CHANGED
@@ -32,6 +32,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var cli_exports = {};
33
33
  __export(cli_exports, {
34
34
  computeDeployDiff: () => computeDeployDiff,
35
+ guessMimeType: () => guessMimeType,
36
+ isAllowedDeployExt: () => isAllowedDeployExt,
37
+ isBinaryDeployExt: () => isBinaryDeployExt,
35
38
  makeRequest: () => makeRequest,
36
39
  normalizeRelativePath: () => normalizeRelativePath,
37
40
  parseArgs: () => parseArgs,
@@ -191,7 +194,11 @@ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
191
194
  ".xml",
192
195
  ".webmanifest",
193
196
  ".map",
194
- ".wasm"
197
+ ".wasm",
198
+ // 3D 에셋: .glb/.bin 은 바이너리, .gltf 는 JSON(텍스트)
199
+ ".glb",
200
+ ".gltf",
201
+ ".bin"
195
202
  ]);
196
203
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
197
204
  ".png",
@@ -213,8 +220,45 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
213
220
  ".webm",
214
221
  ".pdf",
215
222
  ".zip",
216
- ".wasm"
223
+ ".wasm",
224
+ ".glb",
225
+ ".bin"
217
226
  ]);
227
+ var MIME_BY_EXT = {
228
+ ".png": "image/png",
229
+ ".jpg": "image/jpeg",
230
+ ".jpeg": "image/jpeg",
231
+ ".gif": "image/gif",
232
+ ".webp": "image/webp",
233
+ ".avif": "image/avif",
234
+ ".ico": "image/x-icon",
235
+ ".svg": "image/svg+xml",
236
+ ".woff": "font/woff",
237
+ ".woff2": "font/woff2",
238
+ ".ttf": "font/ttf",
239
+ ".otf": "font/otf",
240
+ ".eot": "application/vnd.ms-fontobject",
241
+ ".mp3": "audio/mpeg",
242
+ ".wav": "audio/wav",
243
+ ".ogg": "audio/ogg",
244
+ ".mp4": "video/mp4",
245
+ ".webm": "video/webm",
246
+ ".pdf": "application/pdf",
247
+ ".zip": "application/zip",
248
+ ".wasm": "application/wasm",
249
+ ".glb": "model/gltf-binary",
250
+ ".gltf": "model/gltf+json",
251
+ ".bin": "application/octet-stream"
252
+ };
253
+ function guessMimeType(filePath) {
254
+ return MIME_BY_EXT[path2.extname(filePath).toLowerCase()] || "application/octet-stream";
255
+ }
256
+ function isAllowedDeployExt(filePath) {
257
+ return ALLOWED_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
258
+ }
259
+ function isBinaryDeployExt(filePath) {
260
+ return BINARY_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
261
+ }
218
262
  var WEB_FILE_MAX_STORED_BYTES = 6 * 1024 * 1024 - 64 * 1024;
219
263
  function sha256Hex(value) {
220
264
  return crypto.createHash("sha256").update(value, "utf8").digest("hex");
@@ -305,6 +349,167 @@ function validateFileSizes(files) {
305
349
  warn("\uC62C\uB9AC\uACE0 URL \uB85C \uCC38\uC870\uD558\uC138\uC694. \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0\uB294 \uD30C\uC77C\uB2F9 \uACE0\uC815 \uC0C1\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
306
350
  process.exit(1);
307
351
  }
352
+ async function requestDeployPresign(baseStorageUrl, headers, files, timeoutMs, dev = false) {
353
+ const body = JSON.stringify(dev ? { files, dev: true } : { files });
354
+ const res = await makeRequest(`${baseStorageUrl}/deploy/presign`, "POST", headers, body, { timeoutMs });
355
+ if (res.status === 404) return null;
356
+ if (res.status < 200 || res.status >= 300) {
357
+ const data = res.data;
358
+ const msg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : `HTTP ${res.status}`;
359
+ throw new Error(`presign \uC694\uCCAD \uC2E4\uD328: ${msg}`);
360
+ }
361
+ const parsed = res.data;
362
+ return parsed?.urls ?? [];
363
+ }
364
+ function putToPresignedUrl(uploadUrl, body, contentType, timeoutMs) {
365
+ return new Promise((resolve2, reject) => {
366
+ let parsed;
367
+ try {
368
+ parsed = new URL(uploadUrl);
369
+ } catch {
370
+ reject(new Error("\uC798\uBABB\uB41C \uC5C5\uB85C\uB4DC URL"));
371
+ return;
372
+ }
373
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
374
+ reject(new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC5C5\uB85C\uB4DC URL \uC2A4\uD0B4: ${parsed.protocol}`));
375
+ return;
376
+ }
377
+ const lib = parsed.protocol === "https:" ? https : http;
378
+ const options = {
379
+ hostname: parsed.hostname,
380
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
381
+ path: parsed.pathname + parsed.search,
382
+ method: "PUT",
383
+ family: 4,
384
+ autoSelectFamily: false,
385
+ headers: {
386
+ "Content-Type": contentType || "application/octet-stream",
387
+ "Content-Length": body.length
388
+ }
389
+ };
390
+ const req = lib.request(options, (res) => {
391
+ const chunks = [];
392
+ res.on("data", (chunk) => chunks.push(chunk));
393
+ res.on("end", () => {
394
+ const status = res.statusCode || 0;
395
+ if (status >= 200 && status < 300) {
396
+ resolve2();
397
+ return;
398
+ }
399
+ reject(
400
+ new Error(
401
+ `Object Storage \uC5C5\uB85C\uB4DC \uC2E4\uD328 (HTTP ${status}): ${Buffer.concat(chunks).toString("utf8").slice(0, 200)}`
402
+ )
403
+ );
404
+ });
405
+ });
406
+ req.on("socket", (socket) => {
407
+ socket.setKeepAlive(true, 3e4);
408
+ });
409
+ req.setTimeout(timeoutMs, () => {
410
+ req.destroy(new Error(`\uC5C5\uB85C\uB4DC \uC2DC\uAC04 \uCD08\uACFC (${Math.round(timeoutMs / 1e3)}\uCD08)`));
411
+ });
412
+ req.on("error", reject);
413
+ req.end(body);
414
+ });
415
+ }
416
+ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, timeoutMs) {
417
+ const oversized = diff.upsert.filter((f) => Buffer.byteLength(f.content, "utf8") > WEB_FILE_MAX_STORED_BYTES);
418
+ if (oversized.length === 0) return;
419
+ const largeText = oversized.filter((f) => !f.is_binary);
420
+ const largeBinaries = oversized.filter((f) => f.is_binary);
421
+ if (largeText.length > 0) {
422
+ error(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uD30C\uC77C\uB2F9 \uD06C\uAE30 \uD55C\uB3C4(\uC57D ${formatSize(WEB_FILE_MAX_STORED_BYTES)})\uB97C \uCD08\uACFC\uD55C \uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC788\uC5B4 \uBC30\uD3EC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4:`);
423
+ for (const f of largeText) error(` \u2022 ${f.path} (${formatSize(Buffer.byteLength(f.content, "utf8"))})`);
424
+ log("");
425
+ warn("\uD070 JS/CSS \uB294 \uCF54\uB4DC \uC2A4\uD50C\uB9AC\uD305\uC73C\uB85C \uBD84\uD560\uD558\uC138\uC694. (\uBC14\uC774\uB108\uB9AC\uC640 \uB2EC\uB9AC \uD14D\uC2A4\uD2B8\uB294 \uC624\uBE0C\uC81D\uD2B8 \uC2A4\uD1A0\uB9AC\uC9C0\uB85C \uC624\uD504\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.)");
426
+ process.exit(1);
427
+ }
428
+ if (largeBinaries.length === 0) return;
429
+ const presignFiles = largeBinaries.map((f) => ({
430
+ path: f.path,
431
+ size: Buffer.from(f.content, "base64").length,
432
+ mime_type: guessMimeType(f.path)
433
+ }));
434
+ let presigned;
435
+ try {
436
+ presigned = await requestDeployPresign(baseStorageUrl, headers, presignFiles, timeoutMs);
437
+ } catch (err) {
438
+ error(`\uB300\uC6A9\uB7C9 \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC900\uBE44 \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
439
+ process.exit(1);
440
+ }
441
+ if (presigned === null) {
442
+ error(`\uC11C\uBC84\uAC00 \uB300\uC6A9\uB7C9 \uD30C\uC77C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 \uB2E4\uC74C \uD30C\uC77C\uC744 \uBC30\uD3EC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4(\uC57D ${formatSize(WEB_FILE_MAX_STORED_BYTES)} \uCD08\uACFC):`);
443
+ for (const f of largeBinaries) error(` \u2022 ${f.path} (${formatSize(Buffer.from(f.content, "base64").length)})`);
444
+ log("");
445
+ 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
+ process.exit(1);
447
+ }
448
+ const urlByPath = new Map(presigned.map((p) => [p.path, p.upload_url]));
449
+ info(`\uB300\uC6A9\uB7C9 \uBC14\uC774\uB108\uB9AC ${largeBinaries.length}\uAC1C\uB97C Object Storage \uB85C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4...`);
450
+ 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
+ process.exit(1);
455
+ }
456
+ 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);
462
+ }
463
+ success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC: ${f.path} (${formatSize(bytes.length)})`);
464
+ f.pre_uploaded = true;
465
+ f.size = bytes.length;
466
+ f.content = "";
467
+ }
468
+ }
469
+ async function offloadDevLargeBinaries(baseStorageUrl, headers, files, timeoutMs) {
470
+ const large = files.filter((f) => f.isBinary && Buffer.byteLength(f.content, "utf8") > WEB_FILE_MAX_STORED_BYTES);
471
+ if (large.length === 0) return files;
472
+ const presignFiles = large.map((f) => ({
473
+ path: f.path,
474
+ size: Buffer.from(f.content, "base64").length,
475
+ mime_type: guessMimeType(f.path)
476
+ }));
477
+ let presigned;
478
+ try {
479
+ presigned = await requestDeployPresign(baseStorageUrl, headers, presignFiles, timeoutMs, true);
480
+ } catch (err) {
481
+ error(`dev \uB300\uC6A9\uB7C9 \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC900\uBE44 \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
482
+ process.exit(1);
483
+ }
484
+ if (presigned === null) {
485
+ info("\uC11C\uBC84\uAC00 \uB300\uC6A9\uB7C9 \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uB97C \uBBF8\uC9C0\uC6D0 \u2014 dev \uB294 \uC804\uB7C9 \uC5C5\uB85C\uB4DC\uB85C \uC9C4\uD589\uD569\uB2C8\uB2E4.");
486
+ return files;
487
+ }
488
+ if (!presigned.every((p) => p.object_key.includes("/dev/"))) {
489
+ info("\uC11C\uBC84\uAC00 dev \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uB97C \uBBF8\uC9C0\uC6D0(\uC751\uB2F5 \uD0A4\uAC00 dev \uACBD\uB85C \uC544\uB2D8) \u2014 dev \uB294 \uC804\uB7C9 \uC5C5\uB85C\uB4DC\uB85C \uC9C4\uD589\uD569\uB2C8\uB2E4.");
490
+ return files;
491
+ }
492
+ const urlByPath = new Map(presigned.map((p) => [p.path, p.upload_url]));
493
+ info(`\uB300\uC6A9\uB7C9 \uBC14\uC774\uB108\uB9AC ${large.length}\uAC1C\uB97C dev Object Storage \uB85C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4...`);
494
+ const offloaded = /* @__PURE__ */ new Set();
495
+ for (const f of large) {
496
+ const uploadUrl = urlByPath.get(f.path);
497
+ if (!uploadUrl) {
498
+ error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
499
+ process.exit(1);
500
+ }
501
+ const bytes = Buffer.from(f.content, "base64");
502
+ try {
503
+ await putToPresignedUrl(uploadUrl, bytes, guessMimeType(f.path), timeoutMs);
504
+ } catch (err) {
505
+ error(`dev \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
506
+ process.exit(1);
507
+ }
508
+ success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC(dev): ${f.path} (${formatSize(bytes.length)})`);
509
+ offloaded.add(f.path);
510
+ }
511
+ return files.filter((f) => !offloaded.has(f.path));
512
+ }
308
513
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
309
514
  async function makeRequest(url, method, headers, body, reqOpts = {}) {
310
515
  return new Promise((resolve2, reject) => {
@@ -402,7 +607,6 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
402
607
  error("\uBC30\uD3EC\uD560 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
403
608
  process.exit(1);
404
609
  }
405
- validateFileSizes(files);
406
610
  const totalSize = files.reduce((sum, f) => sum + f.content.length, 0);
407
611
  const sizeKB = (totalSize / 1024).toFixed(1);
408
612
  log(`${colors.green}\u2713${colors.reset} ${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC (${sizeKB} KB)`);
@@ -412,12 +616,14 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
412
616
  const timeoutMs = computeDeployTimeout(totalSize, deployOpts.timeoutMs);
413
617
  log(`${colors.dim}\uD0C0\uC784\uC544\uC6C3: ${Math.round(timeoutMs / 1e3)}\uCD08${colors.reset}`);
414
618
  if (isDev) {
415
- await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy/dev", timeoutMs);
619
+ const toSend = await offloadDevLargeBinaries(baseStorageUrl, headers, files, timeoutMs);
620
+ await fullDeploy(baseStorageUrl, headers, toSend, envLabel, "deploy/dev", timeoutMs);
416
621
  return;
417
622
  }
418
623
  try {
419
624
  const manifest = await tryFetchManifest(baseStorageUrl, headers);
420
625
  if (!manifest) {
626
+ validateFileSizes(files);
421
627
  await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy", timeoutMs);
422
628
  return;
423
629
  }
@@ -429,6 +635,7 @@ ${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.res
429
635
  `);
430
636
  return;
431
637
  }
638
+ await offloadLargeBinaries(baseStorageUrl, headers, diff, envLabel, timeoutMs);
432
639
  info(`\uBCC0\uACBD: ${colors.green}+${diff.upsert.length}${colors.reset} / ${colors.red}-${diff.delete.length}${colors.reset} (\uC804\uCCB4 ${files.length}\uAC1C \uC911)`);
433
640
  const uploadSize = diff.upsert.reduce((s, f) => s + f.content.length, 0);
434
641
  info(`\uC5C5\uB85C\uB4DC \uD06C\uAE30: ${(uploadSize / 1024).toFixed(1)} KB`);
@@ -2653,6 +2860,9 @@ main().catch((err) => {
2653
2860
  // Annotate the CommonJS export names for ESM import in node:
2654
2861
  0 && (module.exports = {
2655
2862
  computeDeployDiff,
2863
+ guessMimeType,
2864
+ isAllowedDeployExt,
2865
+ isBinaryDeployExt,
2656
2866
  makeRequest,
2657
2867
  normalizeRelativePath,
2658
2868
  parseArgs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.41.0",
3
+ "version": "3.43.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",