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 +15 -0
- package/hub/bun.lock +37 -0
- package/hub/index.ts +1 -0
- package/hub/package.json +20 -0
- package/hub/src/auth.ts +19 -0
- package/hub/src/config.ts +43 -0
- package/hub/src/index.ts +131 -0
- package/hub/src/logger.ts +53 -0
- package/hub/src/process_manager.ts +62 -0
- package/hub/src/tunnel.ts +49 -0
- package/hub/tsconfig.json +29 -0
- package/package.json +1 -1
package/hub/README.md
ADDED
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!");
|
package/hub/package.json
ADDED
|
@@ -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
|
+
}
|
package/hub/src/auth.ts
ADDED
|
@@ -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
|
+
}
|
package/hub/src/index.ts
ADDED
|
@@ -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
|
+
}
|