connectbase-client 0.2.0 → 0.3.1
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 +38 -1
- package/dist/cli.js +376 -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
|
|
|
@@ -97,9 +98,45 @@ npx connectbase-client deploy ./dist -s <storage-id> -k <api-key>
|
|
|
97
98
|
| `--storage <id>` | `-s` | Storage ID |
|
|
98
99
|
| `--api-key <key>` | `-k` | API Key |
|
|
99
100
|
| `--base-url <url>` | `-u` | Custom server URL |
|
|
101
|
+
| `--timeout <sec>` | `-t` | Tunnel request timeout in seconds (tunnel only) |
|
|
102
|
+
| `--max-body <MB>` | | Tunnel max body size in MB (tunnel only) |
|
|
100
103
|
| `--help` | `-h` | Show help |
|
|
101
104
|
| `--version` | `-v` | Show version |
|
|
102
105
|
|
|
106
|
+
### Tunnel
|
|
107
|
+
|
|
108
|
+
Expose a local server to the internet through a secure WebSocket tunnel. Useful for sharing local MCP servers, development servers, or any HTTP service.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Expose local port 8084 to the internet
|
|
112
|
+
npx connectbase-client tunnel 8084 -k <api-key>
|
|
113
|
+
|
|
114
|
+
# With environment variable
|
|
115
|
+
export CONNECTBASE_API_KEY=your-api-key
|
|
116
|
+
npx connectbase-client tunnel 8084
|
|
117
|
+
|
|
118
|
+
# For GPU servers or long-running tasks (e.g., image generation)
|
|
119
|
+
npx connectbase-client tunnel 7860 --timeout 300 --max-body 50
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The tunnel creates a public URL like `https://tunnel.connectbase.world/<tunnel-id>/` that proxies all HTTP requests to your local service.
|
|
123
|
+
|
|
124
|
+
**Plan-based limits:** Timeout and body size are clamped to your plan's maximum:
|
|
125
|
+
|
|
126
|
+
| Plan | Max Timeout | Max Body |
|
|
127
|
+
|------|-------------|----------|
|
|
128
|
+
| Free | 60s | 10MB |
|
|
129
|
+
| Starter | 120s | 25MB |
|
|
130
|
+
| Pro | 300s | 50MB |
|
|
131
|
+
| Business | 600s | 100MB |
|
|
132
|
+
|
|
133
|
+
Features:
|
|
134
|
+
- Per-tunnel timeout and body size configuration
|
|
135
|
+
- Automatic reconnection with exponential backoff
|
|
136
|
+
- Request/response logging in terminal
|
|
137
|
+
- Graceful shutdown with Ctrl+C
|
|
138
|
+
- No external dependencies (uses Node.js built-in modules)
|
|
139
|
+
|
|
103
140
|
### Configuration File
|
|
104
141
|
|
|
105
142
|
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,347 @@ 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
|
+
function createWsPongFrame() {
|
|
474
|
+
const maskKey = crypto.randomBytes(4);
|
|
475
|
+
const header = Buffer.alloc(2);
|
|
476
|
+
header[0] = 138;
|
|
477
|
+
header[1] = 128 | 0;
|
|
478
|
+
return Buffer.concat([header, maskKey]);
|
|
479
|
+
}
|
|
480
|
+
var WsFrameParser = class {
|
|
481
|
+
constructor(handlers) {
|
|
482
|
+
this.buffer = Buffer.alloc(0);
|
|
483
|
+
this.onMessage = handlers.onMessage;
|
|
484
|
+
this.onClose = handlers.onClose;
|
|
485
|
+
this.onPing = handlers.onPing;
|
|
486
|
+
}
|
|
487
|
+
feed(chunk) {
|
|
488
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
489
|
+
this.parse();
|
|
490
|
+
}
|
|
491
|
+
parse() {
|
|
492
|
+
while (this.buffer.length >= 2) {
|
|
493
|
+
const firstByte = this.buffer[0];
|
|
494
|
+
const secondByte = this.buffer[1];
|
|
495
|
+
const opcode = firstByte & 15;
|
|
496
|
+
const isMasked = (secondByte & 128) !== 0;
|
|
497
|
+
let payloadLen = secondByte & 127;
|
|
498
|
+
let offset = 2;
|
|
499
|
+
if (payloadLen === 126) {
|
|
500
|
+
if (this.buffer.length < 4) return;
|
|
501
|
+
payloadLen = this.buffer.readUInt16BE(2);
|
|
502
|
+
offset = 4;
|
|
503
|
+
} else if (payloadLen === 127) {
|
|
504
|
+
if (this.buffer.length < 10) return;
|
|
505
|
+
payloadLen = Number(this.buffer.readBigUInt64BE(2));
|
|
506
|
+
offset = 10;
|
|
507
|
+
}
|
|
508
|
+
let maskKey = null;
|
|
509
|
+
if (isMasked) {
|
|
510
|
+
if (this.buffer.length < offset + 4) return;
|
|
511
|
+
maskKey = this.buffer.subarray(offset, offset + 4);
|
|
512
|
+
offset += 4;
|
|
513
|
+
}
|
|
514
|
+
if (this.buffer.length < offset + payloadLen) return;
|
|
515
|
+
let payload = this.buffer.subarray(offset, offset + payloadLen);
|
|
516
|
+
if (maskKey) {
|
|
517
|
+
const unmasked = Buffer.alloc(payloadLen);
|
|
518
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
519
|
+
unmasked[i] = payload[i] ^ maskKey[i % 4];
|
|
520
|
+
}
|
|
521
|
+
payload = unmasked;
|
|
522
|
+
}
|
|
523
|
+
this.buffer = this.buffer.subarray(offset + payloadLen);
|
|
524
|
+
switch (opcode) {
|
|
525
|
+
case 1:
|
|
526
|
+
this.onMessage(payload.toString("utf-8"));
|
|
527
|
+
break;
|
|
528
|
+
case 8:
|
|
529
|
+
this.onClose();
|
|
530
|
+
break;
|
|
531
|
+
case 9:
|
|
532
|
+
this.onPing();
|
|
533
|
+
break;
|
|
534
|
+
case 10:
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function getTunnelServerUrl(baseUrl) {
|
|
541
|
+
if (baseUrl.includes("api.connectbase.world")) {
|
|
542
|
+
return baseUrl.replace("api.connectbase.world", "tunnel.connectbase.world");
|
|
543
|
+
}
|
|
544
|
+
if (baseUrl.includes("localhost:8080")) {
|
|
545
|
+
return baseUrl.replace("localhost:8080", "localhost:8090");
|
|
546
|
+
}
|
|
547
|
+
return baseUrl.replace(/:\d+/, ":8090");
|
|
548
|
+
}
|
|
549
|
+
async function startTunnel(port, config, tunnelOpts) {
|
|
550
|
+
if (!config.apiKey) {
|
|
551
|
+
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");
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
|
|
555
|
+
const parsedUrl = new URL(tunnelServerUrl);
|
|
556
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
557
|
+
let wsPath = `/v1/tunnel/connect?api_key=${encodeURIComponent(config.apiKey)}`;
|
|
558
|
+
if (tunnelOpts?.timeout) {
|
|
559
|
+
wsPath += `&timeout=${tunnelOpts.timeout}`;
|
|
560
|
+
}
|
|
561
|
+
if (tunnelOpts?.maxBody) {
|
|
562
|
+
wsPath += `&max_body=${tunnelOpts.maxBody}`;
|
|
563
|
+
}
|
|
564
|
+
let reconnectAttempts = 0;
|
|
565
|
+
const maxReconnectAttempts = 10;
|
|
566
|
+
let shouldReconnect = true;
|
|
567
|
+
let socket = null;
|
|
568
|
+
const cleanup = () => {
|
|
569
|
+
shouldReconnect = false;
|
|
570
|
+
if (socket) {
|
|
571
|
+
try {
|
|
572
|
+
socket.write(createWsCloseFrame(1e3));
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
setTimeout(() => {
|
|
576
|
+
socket?.destroy();
|
|
577
|
+
process.exit(0);
|
|
578
|
+
}, 500);
|
|
579
|
+
} else {
|
|
580
|
+
process.exit(0);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
process.on("SIGINT", cleanup);
|
|
584
|
+
process.on("SIGTERM", cleanup);
|
|
585
|
+
log(`
|
|
586
|
+
${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
587
|
+
log(`${colors.dim}\uB85C\uCEEC \uD3EC\uD2B8 ${port}\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C\uD569\uB2C8\uB2E4${colors.reset}
|
|
588
|
+
`);
|
|
589
|
+
function connect() {
|
|
590
|
+
const lib = isHttps ? https : http;
|
|
591
|
+
const wsKey = crypto.randomBytes(16).toString("base64");
|
|
592
|
+
const reqOptions = {
|
|
593
|
+
hostname: parsedUrl.hostname,
|
|
594
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
595
|
+
path: wsPath,
|
|
596
|
+
method: "GET",
|
|
597
|
+
family: 4,
|
|
598
|
+
autoSelectFamily: false,
|
|
599
|
+
headers: {
|
|
600
|
+
"Upgrade": "websocket",
|
|
601
|
+
"Connection": "Upgrade",
|
|
602
|
+
"Sec-WebSocket-Key": wsKey,
|
|
603
|
+
"Sec-WebSocket-Version": "13"
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
const req = lib.request(reqOptions);
|
|
607
|
+
req.on("upgrade", (_res, sock, _head) => {
|
|
608
|
+
socket = sock;
|
|
609
|
+
reconnectAttempts = 0;
|
|
610
|
+
const parser = new WsFrameParser({
|
|
611
|
+
onMessage: (data) => {
|
|
612
|
+
try {
|
|
613
|
+
const msg = JSON.parse(data);
|
|
614
|
+
handleMessage(msg, sock, port);
|
|
615
|
+
} catch (e) {
|
|
616
|
+
warn(`\uBA54\uC2DC\uC9C0 \uD30C\uC2F1 \uC2E4\uD328: ${e}`);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
onClose: () => {
|
|
620
|
+
info("\uC11C\uBC84\uAC00 \uC5F0\uACB0\uC744 \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4");
|
|
621
|
+
sock.destroy();
|
|
622
|
+
},
|
|
623
|
+
onPing: () => {
|
|
624
|
+
sock.write(createWsPongFrame());
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
sock.on("data", (chunk) => parser.feed(chunk));
|
|
628
|
+
sock.on("close", () => {
|
|
629
|
+
socket = null;
|
|
630
|
+
if (shouldReconnect) {
|
|
631
|
+
scheduleReconnect();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
sock.on("error", (err) => {
|
|
635
|
+
warn(`\uC5F0\uACB0 \uC5D0\uB7EC: ${err.message}`);
|
|
636
|
+
socket = null;
|
|
637
|
+
if (shouldReconnect) {
|
|
638
|
+
scheduleReconnect();
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
req.on("error", (err) => {
|
|
643
|
+
if (reconnectAttempts === 0) {
|
|
644
|
+
error(`\uD130\uB110 \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328: ${err.message}`);
|
|
645
|
+
}
|
|
646
|
+
if (shouldReconnect) {
|
|
647
|
+
scheduleReconnect();
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
req.on("response", (res) => {
|
|
651
|
+
let body = "";
|
|
652
|
+
res.on("data", (chunk) => body += chunk.toString());
|
|
653
|
+
res.on("end", () => {
|
|
654
|
+
if (res.statusCode === 401) {
|
|
655
|
+
error("\uC778\uC99D \uC2E4\uD328: API Key\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
656
|
+
shouldReconnect = false;
|
|
657
|
+
process.exit(1);
|
|
658
|
+
} else if (res.statusCode === 402) {
|
|
659
|
+
error("\uD560\uB2F9\uB7C9 \uCD08\uACFC: \uD50C\uB79C\uC744 \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694");
|
|
660
|
+
shouldReconnect = false;
|
|
661
|
+
process.exit(1);
|
|
662
|
+
} else {
|
|
663
|
+
error(`\uD130\uB110 \uC11C\uBC84 \uC751\uB2F5 \uC624\uB958 (${res.statusCode}): ${body}`);
|
|
664
|
+
if (shouldReconnect) {
|
|
665
|
+
scheduleReconnect();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
req.end();
|
|
671
|
+
}
|
|
672
|
+
function scheduleReconnect() {
|
|
673
|
+
reconnectAttempts++;
|
|
674
|
+
if (reconnectAttempts > maxReconnectAttempts) {
|
|
675
|
+
error("\uCD5C\uB300 \uC7AC\uC5F0\uACB0 \uC2DC\uB3C4 \uD69F\uC218 \uCD08\uACFC");
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
const delay = Math.min(2e3 * Math.pow(2, reconnectAttempts - 1), 3e4);
|
|
679
|
+
info(`${(delay / 1e3).toFixed(0)}\uCD08 \uD6C4 \uC7AC\uC5F0\uACB0 \uC2DC\uB3C4... (${reconnectAttempts}/${maxReconnectAttempts})`);
|
|
680
|
+
setTimeout(connect, delay);
|
|
681
|
+
}
|
|
682
|
+
function handleMessage(msg, sock, localPort) {
|
|
683
|
+
switch (msg.type) {
|
|
684
|
+
case "tunnel_ready":
|
|
685
|
+
success(`\uD130\uB110 \uD65C\uC131\uD654!`);
|
|
686
|
+
log(`${colors.green}\u2192${colors.reset} URL: ${colors.cyan}${msg.url}${colors.reset}`);
|
|
687
|
+
log(`${colors.green}\u2192${colors.reset} \uB85C\uCEEC: ${colors.cyan}http://localhost:${localPort}${colors.reset}`);
|
|
688
|
+
if (msg.timeout || msg.max_body) {
|
|
689
|
+
log(`${colors.green}\u2192${colors.reset} \uC124\uC815: timeout=${colors.cyan}${msg.timeout}s${colors.reset}, max-body=${colors.cyan}${msg.max_body}MB${colors.reset}`);
|
|
690
|
+
}
|
|
691
|
+
log(`
|
|
692
|
+
${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
|
|
693
|
+
`);
|
|
694
|
+
break;
|
|
695
|
+
case "http_request":
|
|
696
|
+
forwardRequest(msg, sock, localPort);
|
|
697
|
+
break;
|
|
698
|
+
case "tunnel_error":
|
|
699
|
+
error(`\uD130\uB110 \uC5D0\uB7EC: ${msg.message}`);
|
|
700
|
+
break;
|
|
701
|
+
case "ping":
|
|
702
|
+
sock.write(createWsTextFrame(JSON.stringify({ type: "pong" })));
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function forwardRequest(msg, sock, localPort) {
|
|
707
|
+
const requestId = msg.request_id;
|
|
708
|
+
const method = msg.method;
|
|
709
|
+
const reqPath = msg.path;
|
|
710
|
+
const query = msg.query || "";
|
|
711
|
+
const headers = msg.headers || {};
|
|
712
|
+
const bodyBase64 = msg.body;
|
|
713
|
+
const fullPath = query ? `${reqPath}?${query}` : reqPath;
|
|
714
|
+
const localHeaders = {};
|
|
715
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
716
|
+
if (key.toLowerCase() !== "host") {
|
|
717
|
+
localHeaders[key] = value;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
localHeaders["host"] = `localhost:${localPort}`;
|
|
721
|
+
const reqOptions = {
|
|
722
|
+
hostname: "127.0.0.1",
|
|
723
|
+
port: localPort,
|
|
724
|
+
path: fullPath,
|
|
725
|
+
method,
|
|
726
|
+
headers: localHeaders
|
|
727
|
+
};
|
|
728
|
+
const localReq = http.request(reqOptions, (res) => {
|
|
729
|
+
const chunks = [];
|
|
730
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
731
|
+
res.on("end", () => {
|
|
732
|
+
const body = Buffer.concat(chunks);
|
|
733
|
+
const responseHeaders = {};
|
|
734
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
735
|
+
if (value) responseHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
736
|
+
}
|
|
737
|
+
const response = {
|
|
738
|
+
type: "http_response",
|
|
739
|
+
request_id: requestId,
|
|
740
|
+
status: res.statusCode || 200,
|
|
741
|
+
headers: responseHeaders,
|
|
742
|
+
body: body.length > 0 ? body.toString("base64") : ""
|
|
743
|
+
};
|
|
744
|
+
try {
|
|
745
|
+
sock.write(createWsTextFrame(JSON.stringify(response)));
|
|
746
|
+
const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
|
|
747
|
+
log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
|
|
748
|
+
} catch {
|
|
749
|
+
warn(`\uC751\uB2F5 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
localReq.on("error", (err) => {
|
|
754
|
+
const response = {
|
|
755
|
+
type: "http_response",
|
|
756
|
+
request_id: requestId,
|
|
757
|
+
status: 502,
|
|
758
|
+
headers: { "content-type": "application/json" },
|
|
759
|
+
body: Buffer.from(JSON.stringify({ error: `Local server error: ${err.message}` })).toString("base64")
|
|
760
|
+
};
|
|
761
|
+
try {
|
|
762
|
+
sock.write(createWsTextFrame(JSON.stringify(response)));
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
warn(`\uB85C\uCEEC \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328 (${method} ${reqPath}): ${err.message}`);
|
|
766
|
+
});
|
|
767
|
+
if (bodyBase64) {
|
|
768
|
+
localReq.write(Buffer.from(bodyBase64, "base64"));
|
|
769
|
+
}
|
|
770
|
+
localReq.end();
|
|
771
|
+
}
|
|
772
|
+
connect();
|
|
773
|
+
await new Promise(() => {
|
|
774
|
+
});
|
|
775
|
+
}
|
|
434
776
|
function showHelp() {
|
|
435
777
|
log(`
|
|
436
778
|
${colors.cyan}connectbase-client${colors.reset} - Connect Base SDK & CLI
|
|
@@ -441,11 +783,14 @@ ${colors.yellow}\uC0AC\uC6A9\uBC95:${colors.reset}
|
|
|
441
783
|
${colors.yellow}\uBA85\uB839\uC5B4:${colors.reset}
|
|
442
784
|
init \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 (\uC124\uC815 \uD30C\uC77C \uC0DD\uC131)
|
|
443
785
|
deploy <directory> \uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0\uC5D0 \uD30C\uC77C \uBC30\uD3EC
|
|
786
|
+
tunnel <port> \uB85C\uCEEC \uC11C\uBE44\uC2A4\uB97C \uC778\uD130\uB137\uC5D0 \uB178\uCD9C
|
|
444
787
|
|
|
445
788
|
${colors.yellow}\uC635\uC158:${colors.reset}
|
|
446
789
|
-s, --storage <id> \uC2A4\uD1A0\uB9AC\uC9C0 ID
|
|
447
790
|
-k, --api-key <key> API Key
|
|
448
791
|
-u, --base-url <url> \uC11C\uBC84 URL (\uAE30\uBCF8: ${DEFAULT_BASE_URL})
|
|
792
|
+
-t, --timeout <sec> \uD130\uB110 \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (\uCD08, tunnel \uC804\uC6A9)
|
|
793
|
+
--max-body <MB> \uD130\uB110 \uCD5C\uB300 \uBC14\uB514 \uD06C\uAE30 (MB, tunnel \uC804\uC6A9)
|
|
449
794
|
-h, --help \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC
|
|
450
795
|
-v, --version \uBC84\uC804 \uD45C\uC2DC
|
|
451
796
|
|
|
@@ -456,6 +801,12 @@ ${colors.yellow}\uBE60\uB978 \uC2DC\uC791:${colors.reset}
|
|
|
456
801
|
${colors.dim}# 2. \uBC30\uD3EC${colors.reset}
|
|
457
802
|
npm run deploy
|
|
458
803
|
|
|
804
|
+
${colors.dim}# 3. \uD130\uB110 (\uAE30\uBCF8)${colors.reset}
|
|
805
|
+
npx connectbase-client tunnel 3000
|
|
806
|
+
|
|
807
|
+
${colors.dim}# 4. \uD130\uB110 (GPU \uC11C\uBC84 \uB4F1 \uAE34 \uC751\uB2F5 \uC2DC)${colors.reset}
|
|
808
|
+
npx connectbase-client tunnel 7860 --timeout 300 --max-body 50
|
|
809
|
+
|
|
459
810
|
${colors.yellow}\uD658\uACBD\uBCC0\uC218:${colors.reset}
|
|
460
811
|
CONNECTBASE_API_KEY API Key
|
|
461
812
|
CONNECTBASE_STORAGE_ID \uC2A4\uD1A0\uB9AC\uC9C0 ID
|
|
@@ -483,6 +834,10 @@ function parseArgs(args) {
|
|
|
483
834
|
result.options.apiKey = args[++i];
|
|
484
835
|
} else if (arg === "-u" || arg === "--base-url") {
|
|
485
836
|
result.options.baseUrl = args[++i];
|
|
837
|
+
} else if (arg === "-t" || arg === "--timeout") {
|
|
838
|
+
result.options.timeout = args[++i];
|
|
839
|
+
} else if (arg === "--max-body") {
|
|
840
|
+
result.options.maxBody = args[++i];
|
|
486
841
|
} else if (arg === "-h" || arg === "--help") {
|
|
487
842
|
result.options.help = "true";
|
|
488
843
|
} else if (arg === "-v" || arg === "--version") {
|
|
@@ -527,6 +882,27 @@ async function main() {
|
|
|
527
882
|
process.exit(1);
|
|
528
883
|
}
|
|
529
884
|
await deploy(directory, config);
|
|
885
|
+
} else if (parsed.command === "tunnel") {
|
|
886
|
+
const portStr = parsed.args[0];
|
|
887
|
+
if (!portStr) {
|
|
888
|
+
error("\uD3EC\uD2B8 \uBC88\uD638\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC608: npx connectbase-client tunnel 8084");
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
const port = parseInt(portStr, 10);
|
|
892
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
893
|
+
error("\uC720\uD6A8\uD55C \uD3EC\uD2B8 \uBC88\uD638\uB97C \uC785\uB825\uD558\uC138\uC694 (1-65535)");
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
const tunnelOpts = {};
|
|
897
|
+
if (parsed.options.timeout) {
|
|
898
|
+
const t = parseInt(parsed.options.timeout, 10);
|
|
899
|
+
if (!isNaN(t) && t > 0) tunnelOpts.timeout = t;
|
|
900
|
+
}
|
|
901
|
+
if (parsed.options.maxBody) {
|
|
902
|
+
const m = parseInt(parsed.options.maxBody, 10);
|
|
903
|
+
if (!isNaN(m) && m > 0) tunnelOpts.maxBody = m;
|
|
904
|
+
}
|
|
905
|
+
await startTunnel(port, config, tunnelOpts);
|
|
530
906
|
} else {
|
|
531
907
|
error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);
|
|
532
908
|
showHelp();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "connectbase-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|