nio-cat 0.5.0 → 0.6.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/bin/nio.js +150 -13
- package/package.json +2 -1
- package/src/agents/claude.js +3 -1
- package/src/audit.js +55 -0
- package/src/auth.js +175 -0
- package/src/commands.js +12 -1
- package/src/directory.js +56 -0
- package/src/hub.js +60 -12
- package/src/jobs.js +287 -0
- package/src/mcp.js +142 -0
- package/src/memory.js +131 -0
- package/src/proc.js +1 -1
- package/src/push.js +52 -18
- package/src/server.js +93 -31
- package/src/service.js +123 -0
- package/src/tunnel.js +150 -34
- package/web/app/app.js +558 -44
- package/web/app/style.css +73 -11
- package/web/board.css +3 -2
- package/web/board.js +4 -3
- package/web/sw.js +13 -9
package/src/tunnel.js
CHANGED
|
@@ -1,49 +1,165 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// Túnels de Cloudflare.
|
|
3
|
+
// · Túnel amb nom (named tunnel): adreça fixa (p. ex. https://vps.nio.cat). Cal un compte de
|
|
4
|
+
// Cloudflare i que la zona DNS del domini sigui a Cloudflare. Es configura un cop amb
|
|
5
|
+
// `nio tunnel setup <host>` i a partir d'aquí el mòbil s'hi torna a connectar sol.
|
|
6
|
+
// · Quick tunnel: sense compte, però l'adreça (…trycloudflare.com) canvia a cada reinici.
|
|
7
|
+
// És l'opció de reserva.
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawn, spawnSync } = require('child_process');
|
|
5
12
|
const { which, childEnv } = require('./proc');
|
|
13
|
+
const { HOME } = require('./config');
|
|
6
14
|
|
|
15
|
+
const HOST_RE = /^(?=.{4,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
|
16
|
+
const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/i;
|
|
17
|
+
|
|
18
|
+
// Manté cloudflared engegat: si cau, el torna a obrir amb una espera creixent.
|
|
19
|
+
function supervise(bin, args, { onLine, onExit } = {}) {
|
|
20
|
+
let child = null;
|
|
21
|
+
let stopped = false;
|
|
22
|
+
let tries = 0;
|
|
23
|
+
const open = () => {
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
child = spawn(bin, args, { env: childEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
26
|
+
const scan = (d) => String(d).split('\n').forEach((l) => l && onLine && onLine(l));
|
|
27
|
+
child.stdout.on('data', scan);
|
|
28
|
+
child.stderr.on('data', scan);
|
|
29
|
+
child.on('error', () => {});
|
|
30
|
+
child.on('close', (code) => {
|
|
31
|
+
if (stopped) return;
|
|
32
|
+
if (Date.now() - startedAt > 60000) tries = 0;
|
|
33
|
+
const wait = Math.min(60000, 2000 * 2 ** tries++);
|
|
34
|
+
if (onExit) onExit(code, wait);
|
|
35
|
+
setTimeout(() => !stopped && open(), wait).unref();
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
open();
|
|
39
|
+
const stop = () => {
|
|
40
|
+
stopped = true;
|
|
41
|
+
try { child && child.kill(); } catch { /* ja ha acabat */ }
|
|
42
|
+
};
|
|
43
|
+
process.on('exit', stop);
|
|
44
|
+
return { stop, get child() { return child; } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Quick tunnel: l'adreça arriba pel registre de cloudflared. Si es reinicia, n'arriba una altra.
|
|
7
48
|
function startTunnel(port, onUrl) {
|
|
8
49
|
const bin = which('cloudflared');
|
|
9
50
|
if (!bin) return null;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
51
|
+
let last = null;
|
|
52
|
+
return supervise(bin, ['tunnel', '--no-autoupdate', '--url', `http://127.0.0.1:${port}`], {
|
|
53
|
+
onLine: (l) => {
|
|
54
|
+
const m = l.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
55
|
+
if (m && m[0] !== last) {
|
|
56
|
+
last = m[0];
|
|
57
|
+
onUrl(m[0]);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
13
60
|
});
|
|
14
|
-
let found = false;
|
|
15
|
-
const scan = (d) => {
|
|
16
|
-
const m = String(d).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
17
|
-
if (m && !found) {
|
|
18
|
-
found = true;
|
|
19
|
-
onUrl(m[0]);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
child.stdout.on('data', scan);
|
|
23
|
-
child.stderr.on('data', scan);
|
|
24
|
-
process.on('exit', () => child.kill());
|
|
25
|
-
return child;
|
|
26
61
|
}
|
|
27
62
|
|
|
28
|
-
// Túnel amb nom
|
|
29
|
-
function
|
|
63
|
+
// Túnel amb nom configurat amb «nio tunnel setup»: l'adreça és fixa i ja la sabem abans d'obrir-lo.
|
|
64
|
+
function startConfiguredTunnel(t, port, { onReady, onDown } = {}) {
|
|
30
65
|
const bin = which('cloudflared');
|
|
31
66
|
if (!bin) return null;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
67
|
+
const file = writeConfig(t, port);
|
|
68
|
+
let ready = false;
|
|
69
|
+
return supervise(bin, ['tunnel', '--no-autoupdate', '--config', file, 'run', t.id || t.name], {
|
|
70
|
+
onLine: (l) => {
|
|
71
|
+
if (!ready && /Registered tunnel connection/i.test(l)) {
|
|
72
|
+
ready = true;
|
|
73
|
+
onReady && onReady();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
onExit: (code, wait) => {
|
|
77
|
+
ready = false;
|
|
78
|
+
onDown && onDown(code, wait);
|
|
79
|
+
},
|
|
35
80
|
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function configFile() {
|
|
84
|
+
return path.join(HOME, 'cloudflared.yml');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeConfig(t, port) {
|
|
88
|
+
const yml = [
|
|
89
|
+
'# Generat per nio. Es torna a escriure a cada arrencada.',
|
|
90
|
+
`tunnel: ${t.id || t.name}`,
|
|
91
|
+
t.credentials ? `credentials-file: ${t.credentials}` : null,
|
|
92
|
+
'ingress:',
|
|
93
|
+
` - hostname: ${t.host}`,
|
|
94
|
+
` service: http://127.0.0.1:${port}`,
|
|
95
|
+
' - service: http_status:404',
|
|
96
|
+
'',
|
|
97
|
+
].filter((x) => x !== null).join('\n');
|
|
98
|
+
fs.writeFileSync(configFile(), yml, { mode: 0o600 });
|
|
99
|
+
return configFile();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Busca l'identificador d'un túnel que ja existeix.
|
|
103
|
+
function findTunnel(bin, name) {
|
|
104
|
+
const r = spawnSync(bin, ['tunnel', 'list', '--output', 'json', '--name', name], { env: childEnv(), encoding: 'utf8' });
|
|
105
|
+
if (r.status !== 0) return null;
|
|
106
|
+
try {
|
|
107
|
+
const list = JSON.parse(r.stdout || '[]');
|
|
108
|
+
const t = list.find((x) => x.name === name && !x.deleted_at);
|
|
109
|
+
return t ? t.id : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Configuració d'un sol cop: login → create → route dns. Tot passa al compte de Cloudflare
|
|
116
|
+
// de l'usuari i amb les seves credencials (~/.cloudflared). nio només en desa el nom i l'host.
|
|
117
|
+
function setupNamedTunnel({ host, name, log = console.log }) {
|
|
118
|
+
const bin = which('cloudflared');
|
|
119
|
+
if (!bin) throw new Error('Falta cloudflared. Instal·la’l primer (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/).');
|
|
120
|
+
if (!HOST_RE.test(host || '')) throw new Error(`«${host}» no és un nom de domini vàlid (p. ex. vps.nio.cat)`);
|
|
121
|
+
name = name || `nio-${host.split('.')[0]}`;
|
|
122
|
+
if (!NAME_RE.test(name)) throw new Error(`«${name}» no és un nom de túnel vàlid`);
|
|
123
|
+
const run = (args, inherit) => spawnSync(bin, args, { env: childEnv(), stdio: inherit ? 'inherit' : 'pipe', encoding: 'utf8' });
|
|
124
|
+
|
|
125
|
+
const cert = path.join(os.homedir(), '.cloudflared', 'cert.pem');
|
|
126
|
+
if (!fs.existsSync(cert)) {
|
|
127
|
+
log('\n 1/3 · Inicia la sessió a Cloudflare. S’obrirà (o et mostrarà) un enllaç: tria la zona del domini.\n');
|
|
128
|
+
const r = run(['tunnel', 'login'], true);
|
|
129
|
+
if (r.status !== 0) throw new Error('No s’ha pogut iniciar la sessió a Cloudflare');
|
|
130
|
+
} else log(' 1/3 · Ja hi ha una sessió de Cloudflare en aquest ordinador.');
|
|
131
|
+
|
|
132
|
+
let id = findTunnel(bin, name);
|
|
133
|
+
if (!id) {
|
|
134
|
+
log(` 2/3 · Creo el túnel «${name}»…`);
|
|
135
|
+
const r = run(['tunnel', 'create', name]);
|
|
136
|
+
const m = `${r.stdout}${r.stderr}`.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
137
|
+
id = m ? m[0] : findTunnel(bin, name);
|
|
138
|
+
if (r.status !== 0 || !id) throw new Error(`cloudflared no ha pogut crear el túnel:\n${r.stderr || r.stdout}`);
|
|
139
|
+
} else log(` 2/3 · El túnel «${name}» ja existeix (${id}).`);
|
|
140
|
+
|
|
141
|
+
log(` 3/3 · Apunto ${host} cap al túnel (registre CNAME a Cloudflare)…`);
|
|
142
|
+
const r = run(['tunnel', 'route', 'dns', name, host]);
|
|
143
|
+
if (r.status !== 0 && !/already exists|already configured/i.test(`${r.stderr}${r.stdout}`)) {
|
|
144
|
+
throw new Error(`No s’ha pogut crear el registre DNS de ${host}:\n${r.stderr || r.stdout}\nComprova que la zona del domini és al teu compte de Cloudflare.`);
|
|
145
|
+
}
|
|
146
|
+
const credentials = path.join(os.homedir(), '.cloudflared', `${id}.json`);
|
|
147
|
+
return { mode: 'named', name, id, host: host.toLowerCase(), credentials: fs.existsSync(credentials) ? credentials : null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Túnel amb nom creat al tauler de Cloudflare (nio --tunnel-token … --url https://…).
|
|
151
|
+
function startNamedTunnel(token, onReady) {
|
|
152
|
+
const bin = which('cloudflared');
|
|
153
|
+
if (!bin) return null;
|
|
36
154
|
let ready = false;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ready
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
process.on('exit', () => child.kill());
|
|
46
|
-
return child;
|
|
155
|
+
return supervise(bin, ['tunnel', '--no-autoupdate', 'run', '--token', token], {
|
|
156
|
+
onLine: (l) => {
|
|
157
|
+
if (!ready && /Registered tunnel connection/i.test(l)) {
|
|
158
|
+
ready = true;
|
|
159
|
+
onReady();
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
47
163
|
}
|
|
48
164
|
|
|
49
|
-
module.exports = { startTunnel, startNamedTunnel };
|
|
165
|
+
module.exports = { startTunnel, startNamedTunnel, startConfiguredTunnel, setupNamedTunnel, writeConfig, HOST_RE };
|