connectbase-client 0.2.0 → 0.3.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/README.md +23 -1
  2. package/dist/cli.js +334 -0
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -52,7 +52,7 @@ const state = await gameClient.createRoom({
52
52
  - **WebRTC**: Real-time audio/video communication
53
53
  - **Payments**: Subscription and one-time payment support
54
54
  - **AI Streaming**: Real-time AI text generation via WebSocket (Gemini)
55
- - **CLI**: Command-line tool for deploying web storage
55
+ - **CLI**: Command-line tool for deploying web storage and tunneling local services
56
56
 
57
57
  ## CLI
58
58
 
@@ -81,6 +81,7 @@ The `init` command will:
81
81
  |---------|-------------|
82
82
  | `init` | Interactive project setup (creates config, adds deploy script) |
83
83
  | `deploy <dir>` | Deploy files to web storage |
84
+ | `tunnel <port>` | Expose a local service to the internet via WebSocket tunnel |
84
85
 
85
86
  ### Manual Usage
86
87
 
@@ -100,6 +101,27 @@ npx connectbase-client deploy ./dist -s <storage-id> -k <api-key>
100
101
  | `--help` | `-h` | Show help |
101
102
  | `--version` | `-v` | Show version |
102
103
 
104
+ ### Tunnel
105
+
106
+ Expose a local server to the internet through a secure WebSocket tunnel. Useful for sharing local MCP servers, development servers, or any HTTP service.
107
+
108
+ ```bash
109
+ # Expose local port 8084 to the internet
110
+ npx connectbase-client tunnel 8084 -k <api-key>
111
+
112
+ # With environment variable
113
+ export CONNECTBASE_API_KEY=your-api-key
114
+ npx connectbase-client tunnel 8084
115
+ ```
116
+
117
+ The tunnel creates a public URL like `https://tunnel.connectbase.world/<tunnel-id>/` that proxies all HTTP requests to your local service.
118
+
119
+ Features:
120
+ - Automatic reconnection with exponential backoff
121
+ - Request/response logging in terminal
122
+ - Graceful shutdown with Ctrl+C
123
+ - No external dependencies (uses Node.js built-in modules)
124
+
103
125
  ### Configuration File
104
126
 
105
127
  The `init` command creates `.connectbaserc` automatically. You can also create it manually:
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli.ts
27
27
  var fs = __toESM(require("fs"));
28
28
  var path = __toESM(require("path"));
29
+ var crypto = __toESM(require("crypto"));
29
30
  var https = __toESM(require("https"));
30
31
  var http = __toESM(require("http"));
31
32
  var readline = __toESM(require("readline"));
@@ -431,6 +432,326 @@ function addDeployScript(deployDir) {
431
432
  warn("package.json \uC218\uC815\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
432
433
  }
433
434
  }
435
+ function createWsTextFrame(payload) {
436
+ const data = Buffer.from(payload, "utf-8");
437
+ const len = data.length;
438
+ const maskKey = crypto.randomBytes(4);
439
+ let header;
440
+ if (len < 126) {
441
+ header = Buffer.alloc(2);
442
+ header[0] = 129;
443
+ header[1] = 128 | len;
444
+ } else if (len < 65536) {
445
+ header = Buffer.alloc(4);
446
+ header[0] = 129;
447
+ header[1] = 128 | 126;
448
+ header.writeUInt16BE(len, 2);
449
+ } else {
450
+ header = Buffer.alloc(10);
451
+ header[0] = 129;
452
+ header[1] = 128 | 127;
453
+ header.writeBigUInt64BE(BigInt(len), 2);
454
+ }
455
+ const masked = Buffer.alloc(data.length);
456
+ for (let i = 0; i < data.length; i++) {
457
+ masked[i] = data[i] ^ maskKey[i % 4];
458
+ }
459
+ return Buffer.concat([header, maskKey, masked]);
460
+ }
461
+ function createWsCloseFrame(code) {
462
+ const maskKey = crypto.randomBytes(4);
463
+ const payload = Buffer.alloc(2);
464
+ payload.writeUInt16BE(code, 0);
465
+ const masked = Buffer.alloc(2);
466
+ masked[0] = payload[0] ^ maskKey[0];
467
+ masked[1] = payload[1] ^ maskKey[1];
468
+ const header = Buffer.alloc(2);
469
+ header[0] = 136;
470
+ header[1] = 128 | 2;
471
+ return Buffer.concat([header, maskKey, masked]);
472
+ }
473
+ var WsFrameParser = class {
474
+ constructor(handlers) {
475
+ this.buffer = Buffer.alloc(0);
476
+ this.onMessage = handlers.onMessage;
477
+ this.onClose = handlers.onClose;
478
+ this.onPing = handlers.onPing;
479
+ }
480
+ feed(chunk) {
481
+ this.buffer = Buffer.concat([this.buffer, chunk]);
482
+ this.parse();
483
+ }
484
+ parse() {
485
+ while (this.buffer.length >= 2) {
486
+ const firstByte = this.buffer[0];
487
+ const secondByte = this.buffer[1];
488
+ const opcode = firstByte & 15;
489
+ const isMasked = (secondByte & 128) !== 0;
490
+ let payloadLen = secondByte & 127;
491
+ let offset = 2;
492
+ if (payloadLen === 126) {
493
+ if (this.buffer.length < 4) return;
494
+ payloadLen = this.buffer.readUInt16BE(2);
495
+ offset = 4;
496
+ } else if (payloadLen === 127) {
497
+ if (this.buffer.length < 10) return;
498
+ payloadLen = Number(this.buffer.readBigUInt64BE(2));
499
+ offset = 10;
500
+ }
501
+ let maskKey = null;
502
+ if (isMasked) {
503
+ if (this.buffer.length < offset + 4) return;
504
+ maskKey = this.buffer.subarray(offset, offset + 4);
505
+ offset += 4;
506
+ }
507
+ if (this.buffer.length < offset + payloadLen) return;
508
+ let payload = this.buffer.subarray(offset, offset + payloadLen);
509
+ if (maskKey) {
510
+ const unmasked = Buffer.alloc(payloadLen);
511
+ for (let i = 0; i < payloadLen; i++) {
512
+ unmasked[i] = payload[i] ^ maskKey[i % 4];
513
+ }
514
+ payload = unmasked;
515
+ }
516
+ this.buffer = this.buffer.subarray(offset + payloadLen);
517
+ switch (opcode) {
518
+ case 1:
519
+ this.onMessage(payload.toString("utf-8"));
520
+ break;
521
+ case 8:
522
+ this.onClose();
523
+ break;
524
+ case 9:
525
+ this.onPing();
526
+ break;
527
+ case 10:
528
+ break;
529
+ }
530
+ }
531
+ }
532
+ };
533
+ function getTunnelServerUrl(baseUrl) {
534
+ if (baseUrl.includes("api.connectbase.world")) {
535
+ return baseUrl.replace("api.connectbase.world", "tunnel.connectbase.world");
536
+ }
537
+ if (baseUrl.includes("localhost:8080")) {
538
+ return baseUrl.replace("localhost:8080", "localhost:8090");
539
+ }
540
+ return baseUrl.replace(/:\d+/, ":8090");
541
+ }
542
+ async function startTunnel(port, config) {
543
+ if (!config.apiKey) {
544
+ error("API Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. -k \uC635\uC158 \uB610\uB294 CONNECTBASE_API_KEY \uD658\uACBD\uBCC0\uC218\uB97C \uC124\uC815\uD558\uC138\uC694");
545
+ process.exit(1);
546
+ }
547
+ const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
548
+ const parsedUrl = new URL(tunnelServerUrl);
549
+ const isHttps = parsedUrl.protocol === "https:";
550
+ const wsPath = `/v1/tunnel/connect?api_key=${encodeURIComponent(config.apiKey)}`;
551
+ let reconnectAttempts = 0;
552
+ const maxReconnectAttempts = 10;
553
+ let shouldReconnect = true;
554
+ let socket = null;
555
+ const cleanup = () => {
556
+ shouldReconnect = false;
557
+ if (socket) {
558
+ try {
559
+ socket.write(createWsCloseFrame(1e3));
560
+ } catch {
561
+ }
562
+ setTimeout(() => {
563
+ socket?.destroy();
564
+ process.exit(0);
565
+ }, 500);
566
+ } else {
567
+ process.exit(0);
568
+ }
569
+ };
570
+ process.on("SIGINT", cleanup);
571
+ process.on("SIGTERM", cleanup);
572
+ log(`
573
+ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
574
+ log(`${colors.dim}\uB85C\uCEEC \uD3EC\uD2B8 ${port}\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C\uD569\uB2C8\uB2E4${colors.reset}
575
+ `);
576
+ function connect() {
577
+ const lib = isHttps ? https : http;
578
+ const wsKey = crypto.randomBytes(16).toString("base64");
579
+ const reqOptions = {
580
+ hostname: parsedUrl.hostname,
581
+ port: parsedUrl.port || (isHttps ? 443 : 80),
582
+ path: wsPath,
583
+ method: "GET",
584
+ family: 4,
585
+ autoSelectFamily: false,
586
+ headers: {
587
+ "Upgrade": "websocket",
588
+ "Connection": "Upgrade",
589
+ "Sec-WebSocket-Key": wsKey,
590
+ "Sec-WebSocket-Version": "13"
591
+ }
592
+ };
593
+ const req = lib.request(reqOptions);
594
+ req.on("upgrade", (_res, sock, _head) => {
595
+ socket = sock;
596
+ reconnectAttempts = 0;
597
+ const parser = new WsFrameParser({
598
+ onMessage: (data) => {
599
+ try {
600
+ const msg = JSON.parse(data);
601
+ handleMessage(msg, sock, port);
602
+ } catch (e) {
603
+ warn(`\uBA54\uC2DC\uC9C0 \uD30C\uC2F1 \uC2E4\uD328: ${e}`);
604
+ }
605
+ },
606
+ onClose: () => {
607
+ info("\uC11C\uBC84\uAC00 \uC5F0\uACB0\uC744 \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4");
608
+ sock.destroy();
609
+ },
610
+ onPing: () => {
611
+ const pongMsg = JSON.stringify({ type: "pong" });
612
+ sock.write(createWsTextFrame(pongMsg));
613
+ }
614
+ });
615
+ sock.on("data", (chunk) => parser.feed(chunk));
616
+ sock.on("close", () => {
617
+ socket = null;
618
+ if (shouldReconnect) {
619
+ scheduleReconnect();
620
+ }
621
+ });
622
+ sock.on("error", (err) => {
623
+ warn(`\uC5F0\uACB0 \uC5D0\uB7EC: ${err.message}`);
624
+ socket = null;
625
+ if (shouldReconnect) {
626
+ scheduleReconnect();
627
+ }
628
+ });
629
+ });
630
+ req.on("error", (err) => {
631
+ if (reconnectAttempts === 0) {
632
+ error(`\uD130\uB110 \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328: ${err.message}`);
633
+ }
634
+ if (shouldReconnect) {
635
+ scheduleReconnect();
636
+ }
637
+ });
638
+ req.on("response", (res) => {
639
+ let body = "";
640
+ res.on("data", (chunk) => body += chunk.toString());
641
+ res.on("end", () => {
642
+ if (res.statusCode === 401) {
643
+ error("\uC778\uC99D \uC2E4\uD328: API Key\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
644
+ shouldReconnect = false;
645
+ process.exit(1);
646
+ } else if (res.statusCode === 402) {
647
+ error("\uD560\uB2F9\uB7C9 \uCD08\uACFC: \uD50C\uB79C\uC744 \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694");
648
+ shouldReconnect = false;
649
+ process.exit(1);
650
+ } else {
651
+ error(`\uD130\uB110 \uC11C\uBC84 \uC751\uB2F5 \uC624\uB958 (${res.statusCode}): ${body}`);
652
+ if (shouldReconnect) {
653
+ scheduleReconnect();
654
+ }
655
+ }
656
+ });
657
+ });
658
+ req.end();
659
+ }
660
+ function scheduleReconnect() {
661
+ reconnectAttempts++;
662
+ if (reconnectAttempts > maxReconnectAttempts) {
663
+ error("\uCD5C\uB300 \uC7AC\uC5F0\uACB0 \uC2DC\uB3C4 \uD69F\uC218 \uCD08\uACFC");
664
+ process.exit(1);
665
+ }
666
+ const delay = Math.min(2e3 * Math.pow(2, reconnectAttempts - 1), 3e4);
667
+ info(`${(delay / 1e3).toFixed(0)}\uCD08 \uD6C4 \uC7AC\uC5F0\uACB0 \uC2DC\uB3C4... (${reconnectAttempts}/${maxReconnectAttempts})`);
668
+ setTimeout(connect, delay);
669
+ }
670
+ function handleMessage(msg, sock, localPort) {
671
+ switch (msg.type) {
672
+ case "tunnel_ready":
673
+ success(`\uD130\uB110 \uD65C\uC131\uD654!`);
674
+ log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${msg.url}${colors.reset}`);
675
+ log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
676
+ log(`
677
+ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
678
+ `);
679
+ break;
680
+ case "http_request":
681
+ forwardRequest(msg, sock, localPort);
682
+ break;
683
+ case "tunnel_error":
684
+ error(`\uD130\uB110 \uC5D0\uB7EC: ${msg.message}`);
685
+ break;
686
+ case "ping":
687
+ sock.write(createWsTextFrame(JSON.stringify({ type: "pong" })));
688
+ break;
689
+ }
690
+ }
691
+ function forwardRequest(msg, sock, localPort) {
692
+ const requestId = msg.request_id;
693
+ const method = msg.method;
694
+ const reqPath = msg.path;
695
+ const query = msg.query || "";
696
+ const headers = msg.headers || {};
697
+ const bodyBase64 = msg.body;
698
+ const fullPath = query ? `${reqPath}?${query}` : reqPath;
699
+ const reqOptions = {
700
+ hostname: "127.0.0.1",
701
+ port: localPort,
702
+ path: fullPath,
703
+ method,
704
+ headers: { ...headers, host: `localhost:${localPort}` }
705
+ };
706
+ delete reqOptions.headers["Host"];
707
+ const localReq = http.request(reqOptions, (res) => {
708
+ const chunks = [];
709
+ res.on("data", (chunk) => chunks.push(chunk));
710
+ res.on("end", () => {
711
+ const body = Buffer.concat(chunks);
712
+ const responseHeaders = {};
713
+ for (const [key, value] of Object.entries(res.headers)) {
714
+ if (value) responseHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
715
+ }
716
+ const response = {
717
+ type: "http_response",
718
+ request_id: requestId,
719
+ status: res.statusCode || 200,
720
+ headers: responseHeaders,
721
+ body: body.length > 0 ? body.toString("base64") : ""
722
+ };
723
+ try {
724
+ sock.write(createWsTextFrame(JSON.stringify(response)));
725
+ const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
726
+ log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
727
+ } catch {
728
+ warn(`\uC751\uB2F5 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
729
+ }
730
+ });
731
+ });
732
+ localReq.on("error", (err) => {
733
+ const response = {
734
+ type: "http_response",
735
+ request_id: requestId,
736
+ status: 502,
737
+ headers: { "content-type": "application/json" },
738
+ body: Buffer.from(JSON.stringify({ error: `Local server error: ${err.message}` })).toString("base64")
739
+ };
740
+ try {
741
+ sock.write(createWsTextFrame(JSON.stringify(response)));
742
+ } catch {
743
+ }
744
+ warn(`\uB85C\uCEEC \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328 (${method} ${reqPath}): ${err.message}`);
745
+ });
746
+ if (bodyBase64) {
747
+ localReq.write(Buffer.from(bodyBase64, "base64"));
748
+ }
749
+ localReq.end();
750
+ }
751
+ connect();
752
+ await new Promise(() => {
753
+ });
754
+ }
434
755
  function showHelp() {
435
756
  log(`
436
757
  ${colors.cyan}connectbase-client${colors.reset} - Connect Base SDK & CLI
@@ -441,6 +762,7 @@ ${colors.yellow}\uC0AC\uC6A9\uBC95:${colors.reset}
441
762
  ${colors.yellow}\uBA85\uB839\uC5B4:${colors.reset}
442
763
  init \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 (\uC124\uC815 \uD30C\uC77C \uC0DD\uC131)
443
764
  deploy <directory> \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uD30C\uC77C \uBC30\uD3EC
765
+ tunnel <port> \uB85C\uCEEC \uC11C\uBE44\uC2A4\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C
444
766
 
445
767
  ${colors.yellow}\uC635\uC158:${colors.reset}
446
768
  -s, --storage <id> \uC2A4\uD1A0\uB9AC\uC9C0 ID
@@ -527,6 +849,18 @@ async function main() {
527
849
  process.exit(1);
528
850
  }
529
851
  await deploy(directory, config);
852
+ } else if (parsed.command === "tunnel") {
853
+ const portStr = parsed.args[0];
854
+ if (!portStr) {
855
+ error("\uD3EC\uD2B8 \uBC88\uD638\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC608: npx connectbase-client tunnel 8084");
856
+ process.exit(1);
857
+ }
858
+ const port = parseInt(portStr, 10);
859
+ if (isNaN(port) || port < 1 || port > 65535) {
860
+ error("\uC720\uD6A8\uD55C \uD3EC\uD2B8 \uBC88\uD638\uB97C \uC785\uB825\uD558\uC138\uC694 (1-65535)");
861
+ process.exit(1);
862
+ }
863
+ await startTunnel(port, config);
530
864
  } else {
531
865
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);
532
866
  showHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,7 +45,8 @@
45
45
  "deploy",
46
46
  "ai",
47
47
  "streaming",
48
- "realtime"
48
+ "realtime",
49
+ "tunnel"
49
50
  ],
50
51
  "author": "",
51
52
  "license": "MIT",