voidconnect 0.1.13 → 0.1.15

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/hub/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # hub
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/hub/bun.lock ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "hub",
7
+ "dependencies": {
8
+ "@types/qrcode-terminal": "^0.12.2",
9
+ "hono": "^4.11.4",
10
+ "qrcode-terminal": "^0.12.0",
11
+ },
12
+ "devDependencies": {
13
+ "@types/bun": "^1.3.5",
14
+ },
15
+ "peerDependencies": {
16
+ "typescript": "^5",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
22
+
23
+ "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
24
+
25
+ "@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="],
26
+
27
+ "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
28
+
29
+ "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
30
+
31
+ "qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="],
32
+
33
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
34
+
35
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
36
+ }
37
+ }
package/hub/index.ts ADDED
@@ -0,0 +1 @@
1
+ console.log("Hello via Bun!");
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "hub",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "private": true,
6
+ "devDependencies": {
7
+ "@types/bun": "^1.3.5"
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5"
11
+ },
12
+ "scripts": {
13
+ "start": "bun run src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "@types/qrcode-terminal": "^0.12.2",
17
+ "hono": "^4.11.4",
18
+ "qrcode-terminal": "^0.12.0"
19
+ }
20
+ }
@@ -0,0 +1,19 @@
1
+
2
+ import { type Context, type Next } from 'hono';
3
+
4
+ export async function authMiddleware(c: Context, next: Next) {
5
+ const hubPassword = process.env.HUB_PASSWORD;
6
+
7
+ if (!hubPassword) {
8
+ await next();
9
+ return;
10
+ }
11
+
12
+ const authHeader = c.req.header('Authorization');
13
+
14
+ if (!authHeader || authHeader !== `Bearer ${hubPassword}`) {
15
+ return c.json({ error: 'Unauthorized' }, 401);
16
+ }
17
+
18
+ await next();
19
+ }
@@ -0,0 +1,43 @@
1
+
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ export interface ProjectConfig {
6
+ name: string;
7
+ path: string;
8
+ }
9
+
10
+ export interface HubConfig {
11
+ device_name: string;
12
+ password_hash: string | null;
13
+ projects: ProjectConfig[];
14
+ }
15
+
16
+ export async function loadConfig(): Promise<HubConfig> {
17
+ // Check for local run mode (single project via environment variable)
18
+ const projectPath = process.env.HUB_PROJECT_PATH;
19
+ const projectName = process.env.HUB_PROJECT_NAME;
20
+
21
+ if (projectPath && projectName) {
22
+ return {
23
+ device_name: process.env.HUB_DEVICE_NAME || 'VoidConnect Hub',
24
+ password_hash: process.env.HUB_PASSWORD || null,
25
+ projects: [{ name: projectName, path: projectPath }]
26
+ };
27
+ }
28
+
29
+ // Normal mode: load from config file
30
+ const configPath = join(homedir(), '.voidconnect', 'hub_config.json');
31
+ const file = Bun.file(configPath);
32
+
33
+ if (await file.exists()) {
34
+ const content = await file.json();
35
+ return content as HubConfig;
36
+ }
37
+
38
+ return {
39
+ device_name: 'VoidConnect Hub',
40
+ password_hash: null,
41
+ projects: []
42
+ };
43
+ }
@@ -0,0 +1,131 @@
1
+
2
+ import { Hono } from 'hono';
3
+ import { loadConfig } from './config';
4
+ import { authMiddleware } from './auth';
5
+ import { processManager } from './process_manager';
6
+ import qrcode from 'qrcode-terminal';
7
+ import { networkInterfaces } from 'os';
8
+ import { logTraffic, formatRequest, formatResponse } from './logger';
9
+
10
+ const app = new Hono();
11
+
12
+ app.use('*', async (c, next) => {
13
+ if (process.argv.includes('--log')) {
14
+ try {
15
+ await logTraffic('INCOMING_REQUEST', await formatRequest(c.req.raw));
16
+ } catch (e) {
17
+ console.error('Error logging request:', e);
18
+ }
19
+ }
20
+
21
+ await next();
22
+
23
+ if (process.argv.includes('--log')) {
24
+ try {
25
+ await logTraffic('OUTGOING_RESPONSE', await formatResponse(c.res));
26
+ } catch (e) {
27
+ console.error('Error logging response:', e);
28
+ }
29
+ }
30
+ });
31
+
32
+ app.use('/api/*', authMiddleware);
33
+ app.use('/project/*', authMiddleware);
34
+
35
+ app.get('/', (c) => c.text('VoidConnect Hub Running'));
36
+
37
+ app.get('/api/status', (c) => {
38
+ return c.json({ status: 'running', device: process.env.HUB_DEVICE_NAME || 'Unknown' });
39
+ });
40
+
41
+ app.get('/api/projects', async (c) => {
42
+ const config = await loadConfig();
43
+ return c.json(config.projects);
44
+ });
45
+
46
+ app.all('/project/:name/*', async (c) => {
47
+ const name = c.req.param('name');
48
+ const config = await loadConfig();
49
+ const project = config.projects.find(p => p.name === name);
50
+
51
+ if (!project) {
52
+ return c.text('Project not found', 404);
53
+ }
54
+
55
+ try {
56
+ const port = await processManager.startProject(project.name, project.path);
57
+
58
+ const path = c.req.path.replace(`/project/${name}`, '') || '/';
59
+
60
+ const targetUrlObj = new URL(`http://127.0.0.1:${port}${path}`);
61
+
62
+ const originalUrl = new URL(c.req.url);
63
+ originalUrl.searchParams.forEach((value, key) => {
64
+ targetUrlObj.searchParams.set(key, value);
65
+ });
66
+
67
+ targetUrlObj.searchParams.set('directory', project.path);
68
+
69
+ const targetUrl = targetUrlObj.toString();
70
+
71
+ const newReq = new Request(targetUrl, {
72
+ method: c.req.method,
73
+ headers: c.req.header(),
74
+ body: c.req.raw.body,
75
+ });
76
+
77
+ if (process.argv.includes('--log')) {
78
+ await logTraffic('PROXY_REQUEST_OUTGOING', await formatRequest(newReq));
79
+ }
80
+ const response = await fetch(newReq);
81
+ if (process.argv.includes('--log')) {
82
+ await logTraffic('PROXY_RESPONSE_INCOMING', await formatResponse(response));
83
+ }
84
+
85
+ return response;
86
+ } catch (e) {
87
+ console.error(`[Proxy] Error:`, e);
88
+ return c.text(`Global Error: ${e}`, 500);
89
+ }
90
+ });
91
+
92
+ const port = parseInt(process.env.PORT || '3000');
93
+
94
+ if (process.argv.includes('--tunnel') || process.argv.includes('--cf')) {
95
+ import('./tunnel').then(({ startTunnel }) => {
96
+ startTunnel(port);
97
+ });
98
+ } else {
99
+ (async () => {
100
+ const nets = networkInterfaces();
101
+ let localIp = 'localhost';
102
+
103
+ for (const name of Object.keys(nets)) {
104
+ const interfaces = nets[name];
105
+ if (interfaces) {
106
+ for (const net of interfaces) {
107
+ if (net.family === 'IPv4' && !net.internal) {
108
+ localIp = net.address;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ if (localIp !== 'localhost') break;
114
+ }
115
+
116
+ const config = await loadConfig();
117
+ const name = process.env.HUB_DEVICE_NAME || config.device_name || 'My PC';
118
+ const url = `http://${localIp}:${port}`;
119
+
120
+ console.log('\n');
121
+ qrcode.generate(JSON.stringify({ name, url }), { small: true });
122
+ console.log(`\nHub Online: ${name}`);
123
+ console.log(`URL: ${url}\n`);
124
+ })();
125
+ }
126
+
127
+ const server = Bun.serve({
128
+ port,
129
+ fetch: app.fetch,
130
+ idleTimeout: 255,
131
+ });
@@ -0,0 +1,53 @@
1
+
2
+ import { appendFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+
5
+ const LOG_FILE = join(process.cwd(), 'hub_traffic.log');
6
+
7
+ export async function logTraffic(type: string, data: any) {
8
+ const timestamp = new Date().toISOString();
9
+ const logEntry = `[${timestamp}] [${type}]\n${JSON.stringify(data, null, 2)}\n\n--------------------------------------------------\n\n`;
10
+ try {
11
+ await appendFile(LOG_FILE, logEntry);
12
+ } catch (err) {
13
+ console.error('Failed to write to log file:', err);
14
+ }
15
+ }
16
+
17
+ export async function formatRequest(req: Request) {
18
+ let body = '(empty)';
19
+ try {
20
+ if (req.body) {
21
+ // we clone to not consume the stream needed for processing
22
+ body = await req.clone().text();
23
+ }
24
+ } catch (e) {
25
+ body = `(stream/error reading body: ${e})`;
26
+ }
27
+
28
+ return {
29
+ url: req.url,
30
+ method: req.method,
31
+ headers: Object.fromEntries(req.headers.entries()),
32
+ body
33
+ };
34
+ }
35
+
36
+ export async function formatResponse(res: Response) {
37
+ let body = '(empty)';
38
+ try {
39
+ // Cloning response to read body without consuming the original stream being sent to client
40
+ if (res.body) {
41
+ body = await res.clone().text();
42
+ }
43
+ } catch (e) {
44
+ body = `(stream/error reading body: ${e})`;
45
+ }
46
+
47
+ return {
48
+ status: res.status,
49
+ statusText: res.statusText,
50
+ headers: Object.fromEntries(res.headers.entries()),
51
+ body
52
+ };
53
+ }
@@ -0,0 +1,62 @@
1
+
2
+ import { spawn } from 'bun';
3
+
4
+ export class ProcessManager {
5
+ private processes: Map<string, any> = new Map();
6
+ private ports: Map<string, number> = new Map();
7
+ private startPort = 4100;
8
+
9
+ constructor() { }
10
+
11
+ async startProject(name: string, path: string): Promise<number> {
12
+
13
+ if (this.ports.has(name)) {
14
+ const existingPort = this.ports.get(name)!;
15
+ return existingPort;
16
+ }
17
+
18
+ const port = this.startPort + this.processes.size;
19
+
20
+ const p = spawn(["opencode", "serve", "--port", port.toString(), "--hostname", "0.0.0.0"], {
21
+ cwd: path,
22
+ env: { ...process.env, OPENCODE_CLIENT: 'mobile' },
23
+ stdout: 'ignore',
24
+ stderr: 'ignore',
25
+ });
26
+
27
+ this.processes.set(name, p);
28
+ this.ports.set(name, port);
29
+
30
+ let attempts = 0;
31
+ const maxAttempts = 50;
32
+
33
+ while (attempts < maxAttempts) {
34
+ try {
35
+ const healthRes = await fetch(`http://127.0.0.1:${port}/global/health`);
36
+ if (healthRes.ok) {
37
+ break;
38
+ }
39
+ } catch (e) {
40
+ }
41
+ await new Promise(r => setTimeout(r, 100));
42
+ attempts++;
43
+ }
44
+
45
+ if (attempts >= maxAttempts) {
46
+ console.error(`[ProcessManager] WARNING: Project ${name} may not have started correctly on port ${port}`);
47
+ }
48
+
49
+ return port;
50
+ }
51
+
52
+ async stopAll() {
53
+ for (const [name, p] of this.processes) {
54
+ console.log(`Stopping ${name}...`);
55
+ p.kill();
56
+ }
57
+ this.processes.clear();
58
+ this.ports.clear();
59
+ }
60
+ }
61
+
62
+ export const processManager = new ProcessManager();
@@ -0,0 +1,49 @@
1
+
2
+ import { spawn } from 'bun';
3
+ import qrcode from 'qrcode-terminal';
4
+ import { loadConfig } from './config';
5
+
6
+ export async function startTunnel(port: number): Promise<string> {
7
+ console.log(`Starting Cloudflare tunnel...`);
8
+
9
+ const proc = spawn(["cloudflared", "tunnel", "--url", `http://localhost:${port}`], {
10
+ stdout: 'pipe',
11
+ stderr: 'pipe',
12
+ });
13
+
14
+ return new Promise((resolve, reject) => {
15
+ let found = false;
16
+
17
+ const reader = proc.stderr.getReader();
18
+ const decoder = new TextDecoder();
19
+
20
+ async function read() {
21
+ while (true) {
22
+ const { done, value } = await reader.read();
23
+ if (done) break;
24
+
25
+ const text = decoder.decode(value);
26
+ // Silence output unless debug needed
27
+ // console.log(text);
28
+
29
+ const match = text.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
30
+ if (match && !found) {
31
+ found = true;
32
+ const url = match[0];
33
+ const config = await loadConfig();
34
+ const name = config.device_name;
35
+
36
+ console.log('\n');
37
+ qrcode.generate(JSON.stringify({ name, url }), { small: true });
38
+ console.log(`\nHub Online: ${name}`);
39
+ console.log(`URL: ${url}\n`);
40
+
41
+ resolve(url);
42
+ }
43
+ }
44
+ if (!found) reject("Failed to get tunnel URL");
45
+ }
46
+
47
+ read();
48
+ });
49
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidconnect",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "NPM wrapper for voidconnect-cli",
5
5
  "bin": {
6
6
  "voidconnect": "bin/voidconnect.js"