connectbase-client 3.41.0 → 3.42.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 +18 -6
- package/dist/cli.js +169 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,20 +3,32 @@
|
|
|
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.42.0] - 2026-07-02
|
|
7
|
+
|
|
8
|
+
### Added — 웹 배포 대용량 바이너리 Object Storage 직접 업로드 (플랫폼 이슈 019f221b)
|
|
9
|
+
|
|
10
|
+
3.41.0 의 fail-fast 를 이어, 한도를 넘는 **바이너리**를 배포할 수 있게 했다. 파일당 6 MiB(TiDB
|
|
11
|
+
단일 엔트리 한도)를 넘는 바이너리(3D 모델·큰 동영상/폰트 등)를 배포하지 못하던 문제 해결.
|
|
12
|
+
|
|
13
|
+
- **대용량 바이너리 직접 업로드**: 한도를 넘는 바이너리는 presigned URL 로 **Object Storage 의 서빙
|
|
14
|
+
경로에 직접** 업로드하고, 배포 요청에는 `pre_uploaded` 메타데이터만 전송한다. web-proxy 가 서빙하는
|
|
15
|
+
바로 그 경로에 안착하므로 동일 origin·참조 재작성/CORS 불필요. (대응 core-server 필요. 미지원 서버면
|
|
16
|
+
3.41.0 과 동일하게 fail-fast.)
|
|
17
|
+
- **fail-fast 유지**: 오프로드 불가한 파일(대용량 **텍스트**, 또는 서버 미지원)은 업로드 전에 즉시 중단.
|
|
18
|
+
- **3D 에셋 확장자 추가**: `.glb`/`.bin`(바이너리), `.gltf`(JSON)을 허용 확장자에 추가 +
|
|
19
|
+
정확한 MIME(`model/gltf-binary` 등). 이제 3D 모델을 확장자 위장 없이 번들에 그대로 둘 수 있다.
|
|
20
|
+
|
|
6
21
|
## [3.41.0] - 2026-07-02
|
|
7
22
|
|
|
8
23
|
### Added — 웹 배포 파일당 크기 한도 사전 검증 (fail-fast)
|
|
9
24
|
|
|
10
25
|
`connectbase deploy` 가 파일을 수집한 직후, 서버로 업로드하기 **전에** 각 파일이 웹 스토리지
|
|
11
26
|
파일당 크기 한도를 넘는지 검사한다(플랫폼 이슈 019f221b). 기존에는 전량 수집·업로드를 마친
|
|
12
|
-
뒤에야 서버에서 원시 DB 오류(`Error 8025 (HY000): entry too large`)로 실패해 원인 파악이
|
|
13
|
-
어려웠다.
|
|
27
|
+
뒤에야 서버에서 원시 DB 오류(`Error 8025 (HY000): entry too large`)로 실패해 원인 파악이 어려웠다.
|
|
14
28
|
|
|
15
|
-
- 초과 파일이 있으면
|
|
16
|
-
파일 스토리지(`cb.storage`) 사용을 안내한 뒤 업로드 없이 즉시 종료한다.
|
|
29
|
+
- 초과 파일이 있으면 어떤 파일이 얼마나 큰지(바이너리는 원본→base64 크기까지) 나열하고 즉시 종료한다.
|
|
17
30
|
- 검사 기준: 파일 내용은 DB(TiDB Text 컬럼)에 저장되며 단일 엔트리 한도 6 MiB 를 넘을 수 없다.
|
|
18
|
-
바이너리는 base64(약 1.33x)로 저장되므로 원본 기준 실질
|
|
19
|
-
- 텍스트 파일은 UTF-8 바이트 기준으로 정확히 계산(멀티바이트 문자 포함).
|
|
31
|
+
바이너리는 base64(약 1.33x)로 저장되므로 원본 기준 실질 상한 약 4.5 MB. 텍스트는 UTF-8 바이트로 정확히 계산.
|
|
20
32
|
|
|
21
33
|
## [3.40.0] - 2026-06-28
|
|
22
34
|
|
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,123 @@ 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) {
|
|
353
|
+
const body = JSON.stringify({ 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
|
+
}
|
|
308
469
|
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
309
470
|
async function makeRequest(url, method, headers, body, reqOpts = {}) {
|
|
310
471
|
return new Promise((resolve2, reject) => {
|
|
@@ -402,7 +563,6 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
|
|
|
402
563
|
error("\uBC30\uD3EC\uD560 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
403
564
|
process.exit(1);
|
|
404
565
|
}
|
|
405
|
-
validateFileSizes(files);
|
|
406
566
|
const totalSize = files.reduce((sum, f) => sum + f.content.length, 0);
|
|
407
567
|
const sizeKB = (totalSize / 1024).toFixed(1);
|
|
408
568
|
log(`${colors.green}\u2713${colors.reset} ${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC (${sizeKB} KB)`);
|
|
@@ -412,12 +572,14 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
|
|
|
412
572
|
const timeoutMs = computeDeployTimeout(totalSize, deployOpts.timeoutMs);
|
|
413
573
|
log(`${colors.dim}\uD0C0\uC784\uC544\uC6C3: ${Math.round(timeoutMs / 1e3)}\uCD08${colors.reset}`);
|
|
414
574
|
if (isDev) {
|
|
575
|
+
validateFileSizes(files);
|
|
415
576
|
await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy/dev", timeoutMs);
|
|
416
577
|
return;
|
|
417
578
|
}
|
|
418
579
|
try {
|
|
419
580
|
const manifest = await tryFetchManifest(baseStorageUrl, headers);
|
|
420
581
|
if (!manifest) {
|
|
582
|
+
validateFileSizes(files);
|
|
421
583
|
await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy", timeoutMs);
|
|
422
584
|
return;
|
|
423
585
|
}
|
|
@@ -429,6 +591,7 @@ ${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.res
|
|
|
429
591
|
`);
|
|
430
592
|
return;
|
|
431
593
|
}
|
|
594
|
+
await offloadLargeBinaries(baseStorageUrl, headers, diff, envLabel, timeoutMs);
|
|
432
595
|
info(`\uBCC0\uACBD: ${colors.green}+${diff.upsert.length}${colors.reset} / ${colors.red}-${diff.delete.length}${colors.reset} (\uC804\uCCB4 ${files.length}\uAC1C \uC911)`);
|
|
433
596
|
const uploadSize = diff.upsert.reduce((s, f) => s + f.content.length, 0);
|
|
434
597
|
info(`\uC5C5\uB85C\uB4DC \uD06C\uAE30: ${(uploadSize / 1024).toFixed(1)} KB`);
|
|
@@ -2653,6 +2816,9 @@ main().catch((err) => {
|
|
|
2653
2816
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2654
2817
|
0 && (module.exports = {
|
|
2655
2818
|
computeDeployDiff,
|
|
2819
|
+
guessMimeType,
|
|
2820
|
+
isAllowedDeployExt,
|
|
2821
|
+
isBinaryDeployExt,
|
|
2656
2822
|
makeRequest,
|
|
2657
2823
|
normalizeRelativePath,
|
|
2658
2824
|
parseArgs,
|