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.
- package/README.md +23 -1
- package/dist/cli.js +334 -0
- 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.
|
|
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",
|