connectbase-client 6.7.0 → 6.9.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,62 @@
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
+ ## [6.9.0] - 2026-10-30
7
+
8
+ ### Added — 확장자 없는 파일 배포: `deploy --include-extensionless`
9
+
10
+ 무작위 이름의 데이터 파일처럼 확장자 없이 서빙해야 하는 파일을 웹 스토리지에 배포할 수 있습니다. 켜면 확장자 없는
11
+ 파일을 바이너리로 올리고(한도를 넘는 큰 파일은 presigned 직접 업로드), 사이트는 `application/octet-stream` 으로
12
+ 바이트 그대로 서빙합니다(추적 스크립트 주입, gzip 없음, Range 지원, 캐시 1시간).
13
+
14
+ ```bash
15
+ npx connectbase-client deploy ./dist --include-extensionless
16
+ ```
17
+
18
+ `.connectbaserc` 에 `"includeExtensionless": true` 를 넣어도 같습니다. 기본값은 지금처럼 제외입니다 — `deploy` 의
19
+ 기본 디렉터리가 `.` 이라 프로젝트 루트를 배포하면 Dockerfile, LICENSE, 키 파일(id_rsa, kubeconfig 등) 같은 확장자
20
+ 없는 파일이 공개될 수 있어서입니다. 켜도 숨김 파일과 폴더(`.env`, `.ssh/` 등)는 여전히 제외되고, `_redirects`,
21
+ `_headers` 는 지금처럼 텍스트 사이트 설정 파일로 올라갑니다. 확장자 없는 파일이 제외되면 제외 목록 경고에 켜는 방법이
22
+ 함께 나옵니다.
23
+
24
+ 서빙은 2026-10-30 이후 서버(manifest 항목의 `binary` 표시)가 필요합니다. 그 전 배포에 들어 있던 확장자 없는 경로는
25
+ 지금처럼 HTML(SPA 라우트)로 다룹니다.
26
+
27
+ ### Added — `.mvx` 허용
28
+
29
+ `.mvx`(사이트 자체 형식의 3D/자료 파일)를 바이너리 허용 확장자로 추가했습니다. `application/octet-stream`, 캐시
30
+ 1시간으로 서빙되며, 큰 파일은 presigned 경로로 자동 오프로드됩니다.
31
+
32
+ ## [6.8.0] - 2026-10-23
33
+
34
+ ### Added — 서버 코드에서 토픽 만들기: `cb.push.createTopic(appId, name, options?)`
35
+
36
+ 방마다 쓰는 토픽(`cc_<room>` 같은)을 서버 코드가 만들어 두고, 기기는 그 뒤에 `subscribeTopic` 으로 구독합니다. 같은
37
+ 이름의 토픽이 이미 있으면 오류 없이 그 토픽을 돌려줍니다(설정은 바꾸지 않습니다) — 트리거가 다시 실행돼도 안전합니다.
38
+ 공개 키(cb_pk_) 단독 인스턴스로는 호출할 수 없고, 함수의 `ctx.cbAdmin` 은 management_scopes 에 `push:write` 가 있어야
39
+ 합니다(platform-issue 01a167dd). 함수는 배포할 때 이 SDK 의 최신 판을 설치하므로, 쓰려면 함수를 다시 배포하세요.
40
+
41
+ ```ts
42
+ // concerts 트리거 함수
43
+ await ctx.cbAdmin.push.createTopic(appId, `cc_${row.room}`, { displayName: row.title })
44
+ ```
45
+
46
+ ### Fixed — 웹 푸시 기기의 토픽 구독과 등록 해제
47
+
48
+ `registerWebPush` 로 등록한 브라우저 기기를 `subscribeTopic` / `unsubscribeTopic` 으로 구독시키지 못했고,
49
+ `unregisterWebPush` 도 실패했습니다. 웹 푸시 기기의 `device_token` 은 endpoint URL(`https://…/send/…`, 슬래시 포함)인데
50
+ SDK 가 그 값을 요청 경로에 넣어 서버 라우트가 맞지 않았습니다(인코딩해도 서버 라우팅 전에 풀립니다). 이제 기기 토큰은
51
+ 요청 본문으로 보냅니다(`POST /v1/public/push/topics/subscribe`, `/topics/unsubscribe`, `/devices/unregister`) — 네이티브
52
+ (FCM, APNs) 기기도 같은 방법으로 동작하며, 호출 방법은 그대로입니다(platform-issue 01a167dd).
53
+
54
+ ```ts
55
+ const device = await cb.push.registerWebPush(subscription)
56
+ await cb.push.subscribeTopic(device.device_token, 'announcements') // 토픽은 콘솔에서 미리 만든다
57
+ ```
58
+
59
+ 이 판은 위 엔드포인트가 있는 서버(2026-10-23 배포)가 필요합니다. 예전 판(경로에 토큰)도 네이티브 기기에서는 계속
60
+ 동작합니다.
61
+
6
62
  ## [6.7.0] - 2026-10-22
7
63
 
8
64
  ### Added — 서버가 검증한 보낸 사람: `message.fromMember`
package/README.md CHANGED
@@ -294,7 +294,8 @@ In CI, read the Secret Key from a repository secret:
294
294
  ### Requirements
295
295
 
296
296
  - `index.html` must exist in the root of the deploy directory
297
- - Supported file types: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`, `.jpg`, `.gif`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.mp3`, `.mp4`, `.pdf`, `.wasm`, 3D assets (`.glb`, `.gltf`, `.bin`), browser ML models (`.onnx`, `.tflite`, `.task`, `.safetensors`), etc.
297
+ - Supported file types: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`, `.jpg`, `.gif`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.mp3`, `.mp4`, `.pdf`, `.wasm`, 3D/data assets (`.glb`, `.gltf`, `.bin`, `.mvx`), browser ML models (`.onnx`, `.tflite`, `.task`, `.safetensors`), etc.
298
+ - Files without an extension (e.g. randomly named data files) are skipped by default so that a project-root deploy does not publish files like `Dockerfile` or `id_rsa`. Pass `--include-extensionless` (or set `"includeExtensionless": true` in `.connectbaserc`) to deploy them as binary; they are served as `application/octet-stream` byte for byte (requires 6.9.0+).
298
299
  - Files with other extensions are skipped, and `deploy` prints the skipped list as a warning. Hidden files and folders (`.env`, `.git/`) are skipped silently.
299
300
  - Binary files over the per-file limit (~6 MB) are uploaded directly to object storage via presigned URLs.
300
301
 
@@ -1108,7 +1109,7 @@ try {
1108
1109
  ```
1109
1110
 
1110
1111
  방송인 여부는 구독할 때 정해집니다. 멤버 역할이나 카테고리 설정을 바꾸면 다시 구독(또는 재연결)할 때 반영되고,
1111
- 카테고리 설정은 서버 캐시 때문에 최대 5분 뒤에 반영됩니다. room 채널(v6.7.0)은 room 인가 함수가 `publisher: true` 로
1112
+ 카테고리 설정은 저장하면 몇 초 안에 서버 전체에 퍼집니다. room 채널(v6.7.0)은 room 인가 함수가 `publisher: true` 로
1112
1113
  room 마다 방송인을 정할 수도 있습니다(위 "Rooms").
1113
1114
 
1114
1115
  #### Presence / Typing
@@ -1431,7 +1432,17 @@ const subscription = await registration.pushManager.subscribe({
1431
1432
  userVisibleOnly: true,
1432
1433
  applicationServerKey: vapidKey.public_key
1433
1434
  })
1434
- await cb.push.registerWebPush(subscription)
1435
+ const webDevice = await cb.push.registerWebPush(subscription)
1436
+ // Browser devices subscribe the same way (v6.8.0+: device_token is the endpoint URL)
1437
+ await cb.push.subscribeTopic(webDevice.device_token, 'news')
1438
+ ```
1439
+
1440
+ Topics must exist before devices subscribe; subscribing to an unknown topic fails with 404. Create them in the console,
1441
+ or from server code (v6.8.0+, not available to public-key-only instances):
1442
+
1443
+ ```typescript
1444
+ // e.g. in a database trigger function: one topic per room, safe to call again (returns the existing topic)
1445
+ await ctx.cbAdmin.push.createTopic(ctx.appId, `cc_${room}`, { displayName: title })
1435
1446
  ```
1436
1447
 
1437
1448
  ### WebRTC
package/dist/cli.js CHANGED
@@ -43,6 +43,7 @@ __export(cli_exports, {
43
43
  guessMimeType: () => guessMimeType,
44
44
  isAllowedDeployExt: () => isAllowedDeployExt,
45
45
  isBinaryDeployExt: () => isBinaryDeployExt,
46
+ isExtensionlessDataFile: () => isExtensionlessDataFile,
46
47
  makeRequest: () => makeRequest,
47
48
  normalizeRelativePath: () => normalizeRelativePath,
48
49
  packageManagerCommands: () => packageManagerCommands,
@@ -156,7 +157,7 @@ function handleTunnelError(msg, appId, localPort) {
156
157
  }
157
158
 
158
159
  // src/cli.ts
159
- var VERSION = "6.7.0" ? "6.7.0" : getPackageVersion();
160
+ var VERSION = "6.9.0" ? "6.9.0" : getPackageVersion();
160
161
  var DEFAULT_BASE_URL = "https://api.connectbase.world";
161
162
  var colors = {
162
163
  reset: "\x1B[0m",
@@ -218,6 +219,8 @@ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
218
219
  ".glb",
219
220
  ".gltf",
220
221
  ".bin",
222
+ // 사이트 자체 형식의 3D/자료 파일(바이너리, 2026-10-30). 브라우저는 fetch 로만 읽는다.
223
+ ".mvx",
221
224
  // 브라우저 추론용 ML 모델 (platform-issue 01a0f660). 전부 바이너리다.
222
225
  // .onnx — onnxruntime-web
223
226
  // .tflite — TensorFlow Lite (tfjs-tflite 등)
@@ -253,6 +256,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
253
256
  ".wasm",
254
257
  ".glb",
255
258
  ".bin",
259
+ ".mvx",
256
260
  ".onnx",
257
261
  ".tflite",
258
262
  ".task",
@@ -283,6 +287,7 @@ var MIME_BY_EXT = {
283
287
  ".glb": "model/gltf-binary",
284
288
  ".gltf": "model/gltf+json",
285
289
  ".bin": "application/octet-stream",
290
+ ".mvx": "application/octet-stream",
286
291
  // ML 모델은 등록된 표준 MIME 이 없다. 로더(onnxruntime-web/MediaPipe/transformers.js)는
287
292
  // fetch → ArrayBuffer 로 읽으므로 octet-stream 이면 충분하다.
288
293
  ".onnx": "application/octet-stream",
@@ -305,6 +310,10 @@ function isAllowedDeployExt(filePath) {
305
310
  function isBinaryDeployExt(filePath) {
306
311
  return BINARY_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
307
312
  }
313
+ function isExtensionlessDataFile(filePath) {
314
+ const base = path2.basename(filePath);
315
+ return path2.extname(base) === "" && !ALLOWED_EXTENSIONLESS_FILES.has(base);
316
+ }
308
317
  var WEB_FILE_MAX_STORED_BYTES = 6 * 1024 * 1024 - 64 * 1024;
309
318
  function sha256Hex(value) {
310
319
  return crypto.createHash("sha256").update(value, "utf8").digest("hex");
@@ -365,19 +374,21 @@ function loadConfig() {
365
374
  if (rcContent.storageId) config.storageId = rcContent.storageId;
366
375
  if (rcContent.baseUrl) config.baseUrl = rcContent.baseUrl;
367
376
  if (rcContent.deployDir) config.deployDir = rcContent.deployDir;
377
+ if (rcContent.includeExtensionless === true)
378
+ config.includeExtensionless = true;
368
379
  } catch {
369
380
  warn(".connectbaserc \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
370
381
  }
371
382
  }
372
383
  return config;
373
384
  }
374
- function collectDeployFiles(dir) {
385
+ function collectDeployFiles(dir, options = {}) {
375
386
  const skipped = [];
376
- const files = collectFiles(dir, dir, skipped);
387
+ const files = collectFiles(dir, dir, skipped, options);
377
388
  skipped.sort();
378
389
  return { files, skipped };
379
390
  }
380
- function collectFiles(dir, baseDir, skipped) {
391
+ function collectFiles(dir, baseDir, skipped, options) {
381
392
  const files = [];
382
393
  const entries = fs2.readdirSync(dir, { withFileTypes: true });
383
394
  for (const entry of entries) {
@@ -385,18 +396,19 @@ function collectFiles(dir, baseDir, skipped) {
385
396
  const relativePath = path2.relative(baseDir, fullPath);
386
397
  if (entry.isDirectory()) {
387
398
  if (!entry.name.startsWith(".")) {
388
- files.push(...collectFiles(fullPath, baseDir, skipped));
399
+ files.push(...collectFiles(fullPath, baseDir, skipped, options));
389
400
  }
390
401
  } else if (entry.isFile()) {
391
402
  const ext = path2.extname(entry.name).toLowerCase();
392
403
  if (entry.name.startsWith(".")) {
393
404
  continue;
394
405
  }
395
- if (!isAllowedDeployExt(entry.name)) {
406
+ const extensionlessData = options.includeExtensionless === true && isExtensionlessDataFile(entry.name);
407
+ if (!extensionlessData && !isAllowedDeployExt(entry.name)) {
396
408
  skipped.push(normalizeRelativePath(relativePath));
397
409
  continue;
398
410
  }
399
- const isBinary = BINARY_EXTENSIONS.has(ext);
411
+ const isBinary = extensionlessData || BINARY_EXTENSIONS.has(ext);
400
412
  let content;
401
413
  if (isBinary) {
402
414
  content = fs2.readFileSync(fullPath).toString("base64");
@@ -438,6 +450,11 @@ function formatSkippedFilesReport(skipped, limit = SKIPPED_FILES_LIST_LIMIT) {
438
450
  lines.push(
439
451
  " \uD544\uC694\uD55C \uD30C\uC77C\uC774\uBA74 \uD5C8\uC6A9 \uD655\uC7A5\uC790\uB85C \uBC14\uAFB8\uAC70\uB098, \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(cb.storage)\uC5D0 \uC62C\uB824 URL \uB85C \uCC38\uC870\uD558\uC138\uC694."
440
452
  );
453
+ if (skipped.some((p) => isExtensionlessDataFile(p))) {
454
+ lines.push(
455
+ ' \uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C\uC740 --include-extensionless (\uB610\uB294 .connectbaserc \uC758 "includeExtensionless": true) \uB85C \uBC14\uC774\uB108\uB9AC(application/octet-stream)\uB85C \uBC30\uD3EC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.'
456
+ );
457
+ }
441
458
  return lines;
442
459
  }
443
460
  function reportSkippedFiles(skipped) {
@@ -766,8 +783,18 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
766
783
  }
767
784
  info(`\uBC30\uD3EC \uC900\uBE44 \uC911... ${dir}`);
768
785
  process.stdout.write(`${colors.dim}\uD30C\uC77C \uC218\uC9D1 \uC911...${colors.reset}`);
769
- const { files, skipped } = collectDeployFiles(dir);
786
+ const { files, skipped } = collectDeployFiles(dir, {
787
+ includeExtensionless: deployOpts.includeExtensionless
788
+ });
770
789
  process.stdout.write(`\r${colors.dim} \r${colors.reset}`);
790
+ if (deployOpts.includeExtensionless) {
791
+ const extensionless = files.filter((f) => isExtensionlessDataFile(f.path));
792
+ if (extensionless.length > 0) {
793
+ info(
794
+ `\uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C ${extensionless.length}\uAC1C\uB97C \uBC14\uC774\uB108\uB9AC(application/octet-stream)\uB85C \uD3EC\uD568\uD569\uB2C8\uB2E4 (--include-extensionless)`
795
+ );
796
+ }
797
+ }
771
798
  if (files.length === 0) {
772
799
  reportSkippedFiles(skipped);
773
800
  error("\uBC30\uD3EC\uD560 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -3670,6 +3697,7 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
3670
3697
  --label <name> Endpoint binding \uC790\uB3D9 \uB4F1\uB85D (tunnel \uC804\uC6A9) \u2014 SDK \uC758 cb.endpoint.call(label) \uD638\uCD9C \uAC00\uB2A5
3671
3698
  --description <text> Endpoint binding \uC124\uBA85 (--label \uB3D9\uBC18 \uC2DC\uB9CC)
3672
3699
  -d, --dev QA \uD658\uACBD\uC5D0\uB9CC \uBC30\uD3EC (deploy \uC804\uC6A9, deprecated \u2014 QA \uBCC4\uCE6D)
3700
+ --include-extensionless \uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C\uB3C4 \uBC14\uC774\uB108\uB9AC(application/octet-stream)\uB85C \uBC30\uD3EC (deploy \uC804\uC6A9, \uAE30\uBCF8: \uC81C\uC678)
3673
3701
  -y, --yes \uBE44\uB300\uD654\uD615 \uCD08\uAE30\uD654 \u2014 \uD504\uB86C\uD504\uD2B8 \uC5C6\uC774 \uAE30\uBCF8\uAC12/\uD50C\uB798\uADF8\uB85C \uC9C4\uD589 (init \uC804\uC6A9, CI/AI \uC5D0\uC774\uC804\uD2B8\uC6A9)
3674
3702
  -a, --app <id> \uAE30\uC874 \uC571 ID \uC9C0\uC815 (init \uC804\uC6A9)
3675
3703
  --secret-key <key> Secret Key (cb_sk_) \uC9C0\uC815 (init/mcp)
@@ -3727,7 +3755,8 @@ ${colors.yellow}\uC124\uC815 \uD30C\uC77C (.connectbaserc):${colors.reset}
3727
3755
  "publicKey": "cb_pk_...",
3728
3756
  "secretKey": "cb_sk_...",
3729
3757
  "storageId": "your-storage-id",
3730
- "deployDir": "./dist"
3758
+ "deployDir": "./dist",
3759
+ "includeExtensionless": false ${colors.dim}// true \uBA74 \uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C\uB3C4 \uBC30\uD3EC (= --include-extensionless)${colors.reset}
3731
3760
  }
3732
3761
  `);
3733
3762
  }
@@ -3771,6 +3800,8 @@ function parseArgs(args) {
3771
3800
  result.options.showToken = "true";
3772
3801
  } else if (arg === "-d" || arg === "--dev") {
3773
3802
  result.options.dev = "true";
3803
+ } else if (arg === "--include-extensionless") {
3804
+ result.options.includeExtensionless = "true";
3774
3805
  } else if (arg === "--check") {
3775
3806
  result.options.check = "true";
3776
3807
  } else if (arg === "--skip-docs") {
@@ -3845,7 +3876,9 @@ async function main() {
3845
3876
  process.exit(1);
3846
3877
  }
3847
3878
  const isDev = parsed.options.dev === "true";
3848
- const deployOpts = {};
3879
+ const deployOpts = {
3880
+ includeExtensionless: parsed.options.includeExtensionless === "true" || fileConfig.includeExtensionless === true
3881
+ };
3849
3882
  if (parsed.options.timeout) {
3850
3883
  const t = parseInt(parsed.options.timeout, 10);
3851
3884
  if (!Number.isNaN(t) && t > 0) deployOpts.timeoutMs = t * 1e3;
@@ -3960,6 +3993,7 @@ main().catch((err) => {
3960
3993
  guessMimeType,
3961
3994
  isAllowedDeployExt,
3962
3995
  isBinaryDeployExt,
3996
+ isExtensionlessDataFile,
3963
3997
  makeRequest,
3964
3998
  normalizeRelativePath,
3965
3999
  packageManagerCommands,