@danieltmn/tamnora 0.10.0 → 0.11.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/CHANGELOG.md +20 -0
- package/README.md +7 -14
- package/SECURITY.md +4 -6
- package/harness/packages/cli/src/cli.js +1 -1
- package/harness/packages/cli/src/commands/bridge.js +51 -210
- package/harness/packages/cli/test/tunnel.test.js +5 -1
- package/harness/packages/core/src/config/app.js +4 -6
- package/harness/packages/core/src/index.js +0 -1
- package/harness/packages/core/src/tunnel.js +2 -154
- package/harness/packages/core/test/home.test.js +7 -5
- package/harness/packages/server/public/app.js +23 -42
- package/harness/packages/server/public/index.html +1 -1
- package/harness/packages/server/public/styles.css +53 -2
- package/package.json +1 -1
- package/harness/packages/server/public/relayClient.js +0 -170
- package/harness/packages/server/test/relay.test.js +0 -219
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
|
|
|
5
5
|
El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
|
|
6
6
|
y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
|
|
7
7
|
|
|
8
|
+
## [0.11.1] - 2026-10-09
|
|
9
|
+
|
|
10
|
+
### Arreglado
|
|
11
|
+
|
|
12
|
+
- **Consola en movil**: el input del chat ahora es un campo visible en su propia
|
|
13
|
+
linea (antes flotaba sin fondo y casi no se notaba); las cards de MCP/Skills
|
|
14
|
+
(y tambien proveedores y procesos) llevan las acciones a su propia fila y la
|
|
15
|
+
descripcion se recorta a dos lineas en vez de apretarse contra los botones.
|
|
16
|
+
|
|
17
|
+
## [0.11.0] - 2026-10-09
|
|
18
|
+
|
|
19
|
+
### Quitado
|
|
20
|
+
|
|
21
|
+
- **Tuneles publicos retirados**: `tamnora bridge` ahora soporta unicamente
|
|
22
|
+
**tailscale**. Se elimino el relay propio (`relay/index.php`, el deploy FTPS
|
|
23
|
+
`scripts/deploy-relay.mjs` y el transporte `relayClient.js` de la consola) y el
|
|
24
|
+
proveedor `tunnelmole` dejo de ofrecerse para el acceso remoto (se conserva solo
|
|
25
|
+
para el feature de `preview`). Un `app.json` con `tunnel.provider` en
|
|
26
|
+
`tunnelmole`/`relay` se migra a `tailscale` al leerlo.
|
|
27
|
+
|
|
8
28
|
## [0.10.0] - 2026-10-09
|
|
9
29
|
|
|
10
30
|
### Agregado
|
package/README.md
CHANGED
|
@@ -144,11 +144,10 @@ corre en modo local sin login. Al cambiar el usuario se detiene el server
|
|
|
144
144
|
|
|
145
145
|
### Acceso remoto
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
base SQLite).
|
|
147
|
+
El acceso remoto usa **Tailscale** (VPN mesh privada, rapida y sin exponer nada a
|
|
148
|
+
Internet). El server sigue escuchando en `127.0.0.1`; `tailscale serve` publica
|
|
149
|
+
HTTPS dentro del tailnet y reenvia por loopback, asi que la consola y todas las
|
|
150
|
+
tools funcionan igual que en local, sin sincronizar datos (una sola base SQLite).
|
|
152
151
|
|
|
153
152
|
```bash
|
|
154
153
|
# 1. Instalar Tailscale en esta PC y en el dispositivo remoto (misma cuenta)
|
|
@@ -156,7 +155,7 @@ base SQLite).
|
|
|
156
155
|
tamnora user --name yo --password unaClaveLarga
|
|
157
156
|
|
|
158
157
|
# 2. Server + acceso remoto
|
|
159
|
-
tamnora bridge --start #
|
|
158
|
+
tamnora bridge --start # tailscale serve (URL + QR)
|
|
160
159
|
tamnora bridge --status # https://<pc>.<tailnet>.ts.net
|
|
161
160
|
tamnora bridge --stop # tailscale serve reset
|
|
162
161
|
```
|
|
@@ -165,14 +164,8 @@ tamnora bridge --stop # tailscale serve reset
|
|
|
165
164
|
Sigue protegido por el login del server: configura un usuario antes de exponerlo
|
|
166
165
|
(ver [`SECURITY.md`](SECURITY.md)).
|
|
167
166
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
se conservara unicamente para el feature de *preview*:
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
tamnora bridge --start --tunnel tunnelmole # tunel publico gratis, URL aleatoria
|
|
174
|
-
tamnora bridge --start --tunnel relay # mailbox propio (harness.tamnora.com)
|
|
175
|
-
```
|
|
167
|
+
Los tuneles publicos (tunnelmole, relay) fueron **retirados**; `tunnelmole` se
|
|
168
|
+
conserva solo para el feature de *preview*.
|
|
176
169
|
|
|
177
170
|
### Telegram (canales de mensajeria)
|
|
178
171
|
|
package/SECURITY.md
CHANGED
|
@@ -20,12 +20,10 @@ asi que se lo debe tratar como codigo con acceso a tu usuario.
|
|
|
20
20
|
**usuario** con contrasena fuerte (defensa en profundidad). Como entra por
|
|
21
21
|
loopback, una vez autenticado se alcanza toda la consola y las tools
|
|
22
22
|
(explorador de archivos, shell, git): tratalo como acceso completo a tu PC.
|
|
23
|
-
- **Tuneles publicos (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tratalo como acceso completo a tu maquina. Cierra el tunnel (`tamnora bridge
|
|
28
|
-
--stop`) cuando no lo uses.
|
|
23
|
+
- **Tuneles publicos (retirados)**: las versiones anteriores usaban tunnelmole y
|
|
24
|
+
un relay propio para exponer la consola desde Internet. Se retiraron; el acceso
|
|
25
|
+
remoto hoy es solo por Tailscale. `tunnelmole` sobrevive unicamente para el
|
|
26
|
+
feature de *preview* de dev servers, que no expone la consola.
|
|
29
27
|
- **Credenciales**: las API keys de proveedores se guardan en la base SQLite de
|
|
30
28
|
la casa local (fuera del repo) **en texto plano**; protege el archivo con los
|
|
31
29
|
permisos del sistema. Para no persistirlas, usa `"apiKey": "env:VAR"` y
|
|
@@ -72,7 +72,7 @@ Uso:
|
|
|
72
72
|
tamnora mcp Listar servidores y herramientas MCP
|
|
73
73
|
tamnora server [--init|--start|--reload|--stop|--status|--logs] Server en segundo plano (abre el navegador)
|
|
74
74
|
tamnora user Usuario local (login del server)
|
|
75
|
-
tamnora bridge [--start|--reload|--stop|--status]
|
|
75
|
+
tamnora bridge [--start|--reload|--stop|--status] Acceso remoto con Tailscale + QR
|
|
76
76
|
tamnora autostart install|remove Arranque automatico del server
|
|
77
77
|
tamnora serve Consola web local (primer plano)
|
|
78
78
|
tamnora doctor Verificar Node, casa, puerto y base
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
homeDir,
|
|
3
|
-
ensureHome,
|
|
4
|
-
ensureApp,
|
|
5
|
-
writeApp,
|
|
6
|
-
readApp,
|
|
7
|
-
randomToken,
|
|
8
|
-
} from '../../../core/src/index.js';
|
|
1
|
+
import { homeDir, ensureHome, ensureApp } from '../../../core/src/index.js';
|
|
9
2
|
import { parse, num } from '../lib/util.js';
|
|
10
3
|
import {
|
|
11
4
|
applyHomeOverride,
|
|
@@ -21,17 +14,6 @@ import {
|
|
|
21
14
|
import { startTunnel, probeTailscale, resetTailscaleServe } from '../lib/tunnel.js';
|
|
22
15
|
import { qrTerminal } from '../lib/qr.js';
|
|
23
16
|
|
|
24
|
-
// Tuneles publicos legacy: se retiran en la proxima version. `tunnelmole` se
|
|
25
|
-
// conserva solo para el feature de `preview` (no como proveedor de `bridge`).
|
|
26
|
-
const LEGACY_PROVIDERS = new Set(['tunnelmole', 'tmole', 'relay']);
|
|
27
|
-
|
|
28
|
-
function warnLegacy(provider) {
|
|
29
|
-
const p = String(provider).toLowerCase();
|
|
30
|
-
if (!LEGACY_PROVIDERS.has(p)) return;
|
|
31
|
-
console.log(`Aviso: "${p}" es legacy (tunel publico) y se retirara en la proxima version.`);
|
|
32
|
-
console.log('El camino recomendado es --tunnel tailscale (VPN privada).');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
17
|
const OPTIONS = {
|
|
36
18
|
start: { type: 'boolean' },
|
|
37
19
|
reload: { type: 'boolean' },
|
|
@@ -51,6 +33,8 @@ const readBridgeState = () => bridgeState.read();
|
|
|
51
33
|
const writeBridgeState = (data) => bridgeState.write(data);
|
|
52
34
|
const clearBridgeState = () => bridgeState.clear();
|
|
53
35
|
|
|
36
|
+
const isTailscale = (provider) => provider === 'tailscale' || provider === 'ts';
|
|
37
|
+
|
|
54
38
|
export async function bridgeCommand(argv) {
|
|
55
39
|
const { values } = parse(argv, OPTIONS);
|
|
56
40
|
if (values.help) {
|
|
@@ -73,61 +57,42 @@ export async function bridgeCommand(argv) {
|
|
|
73
57
|
function resolve(values) {
|
|
74
58
|
ensureHome();
|
|
75
59
|
const app = ensureApp();
|
|
60
|
+
const requested = values.tunnel ? String(values.tunnel).toLowerCase() : '';
|
|
76
61
|
return {
|
|
77
62
|
port: num(values.port, app.port || 4517),
|
|
78
|
-
provider:
|
|
63
|
+
provider: requested || String(app.tunnel?.provider || 'tailscale').toLowerCase(),
|
|
79
64
|
app,
|
|
80
65
|
};
|
|
81
66
|
}
|
|
82
67
|
|
|
83
|
-
/** Asegura la configuracion del relay (url + token) en app.json y la devuelve. */
|
|
84
|
-
function ensureRelayConfig(app) {
|
|
85
|
-
const tunnel = { ...app.tunnel };
|
|
86
|
-
if (!tunnel.relayUrl) {
|
|
87
|
-
tunnel.relayUrl = process.env.TAMNORA_RELAY_URL || 'https://harness.tamnora.com/index.php';
|
|
88
|
-
}
|
|
89
|
-
if (!tunnel.token) {
|
|
90
|
-
tunnel.token = process.env.TAMNORA_RELAY_TOKEN || randomToken(24);
|
|
91
|
-
const current = readApp();
|
|
92
|
-
writeApp({ ...current, tunnel: { ...current.tunnel, token: tunnel.token, relayUrl: tunnel.relayUrl } });
|
|
93
|
-
console.log(`Token de relay nuevo (guardado en app.json): ${tunnel.token}`);
|
|
94
|
-
}
|
|
95
|
-
return tunnel;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
68
|
async function cmdStart(values) {
|
|
99
69
|
const { port, provider } = resolve(values);
|
|
100
70
|
if (provider === 'none') {
|
|
101
|
-
console.error('El
|
|
71
|
+
console.error('El acceso remoto esta desactivado (provider "none"). Usa --tunnel tailscale.');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
if (!isTailscale(provider)) {
|
|
75
|
+
console.error(`Proveedor no soportado: "${provider}". El acceso remoto usa Tailscale.`);
|
|
76
|
+
console.error('Los tuneles publicos (tunnelmole/relay) fueron retirados.');
|
|
102
77
|
return 1;
|
|
103
78
|
}
|
|
104
79
|
|
|
105
|
-
//
|
|
106
|
-
// (incluido /api/chat con tool bash): hay que pedirlo explicito.
|
|
80
|
+
// Por la red privada igual conviene un usuario: defensa en profundidad.
|
|
107
81
|
const app = ensureApp();
|
|
108
82
|
if (!app.user?.password && !values.insecure) {
|
|
109
|
-
console.error('No hay usuario configurado:
|
|
83
|
+
console.error('No hay usuario configurado: sin login cualquiera en tu tailnet tendria acceso total.');
|
|
110
84
|
console.error('Crea uno con: tamnora user --name <usuario> --password <clave>');
|
|
111
85
|
console.error('Si igual queres abrirlo sin proteccion: tamnora bridge --insecure');
|
|
112
86
|
return 1;
|
|
113
87
|
}
|
|
114
88
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return 1;
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
warnLegacy(provider);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (provider === 'tunnelmole' || provider === 'tmole') {
|
|
129
|
-
console.log('Aviso: tunnelmole gratuito admite una sola conexion; si abriste el');
|
|
130
|
-
console.log('preview de la consola, cerralo (o el bridge) antes de usar el otro.');
|
|
89
|
+
try {
|
|
90
|
+
const status = await probeTailscale();
|
|
91
|
+
if (!status.connected) throw new Error('Tailscale no esta conectado. Inicia sesion con `tailscale up`.');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(error.message);
|
|
94
|
+
console.error('Instala Tailscale: https://tailscale.com/download');
|
|
95
|
+
return 1;
|
|
131
96
|
}
|
|
132
97
|
|
|
133
98
|
const current = readBridgeState();
|
|
@@ -138,11 +103,11 @@ async function cmdStart(values) {
|
|
|
138
103
|
}
|
|
139
104
|
|
|
140
105
|
const bin = process.argv[1];
|
|
141
|
-
const args = [bin, 'bridge', '--foreground', '--port', String(port)
|
|
106
|
+
const args = [bin, 'bridge', '--foreground', '--port', String(port)];
|
|
142
107
|
if (process.env.TAMNORA_HOME) args.push('--home', process.env.TAMNORA_HOME);
|
|
143
108
|
if (values.verbose) args.push('--verbose');
|
|
144
109
|
const { child, isDead } = spawnDetached(args, { envFlag: 'TAMNORA_BRIDGE_CHILD' });
|
|
145
|
-
console.log(`Abriendo acceso remoto (
|
|
110
|
+
console.log(`Abriendo acceso remoto (tailscale) en el puerto ${port}...`);
|
|
146
111
|
|
|
147
112
|
const state = await waitForState(() => readBridgeState(), {
|
|
148
113
|
childPid: child.pid,
|
|
@@ -160,17 +125,16 @@ async function cmdStart(values) {
|
|
|
160
125
|
return 0;
|
|
161
126
|
}
|
|
162
127
|
|
|
163
|
-
/** URL publica para el celular: la consola agregada al relay con el token. */
|
|
164
|
-
function relayConsoleUrl(extra) {
|
|
165
|
-
const base = String(extra.relayUrl ?? '').replace(/\/+$/, '').replace(/index\.php$/, '').replace(/\/$/, '');
|
|
166
|
-
return `${base}/console/?t=${extra.token}`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
128
|
/**
|
|
170
129
|
* Tailscale es configuracion persistente (`tailscale serve`), no un proceso:
|
|
171
130
|
* no hay health-check ni bucle de reconexion, solo esperar la senal de apagado.
|
|
172
131
|
*/
|
|
173
|
-
async function
|
|
132
|
+
async function runForeground(values) {
|
|
133
|
+
const { port, provider } = resolve(values);
|
|
134
|
+
if (!isTailscale(provider)) {
|
|
135
|
+
console.error(`Proveedor no soportado: "${provider}". El acceso remoto usa Tailscale.`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
174
138
|
const log = createFileLogger(bridgeLogPath());
|
|
175
139
|
let tun;
|
|
176
140
|
try {
|
|
@@ -210,121 +174,9 @@ async function runTailscaleForeground(values, port) {
|
|
|
210
174
|
return 0;
|
|
211
175
|
}
|
|
212
176
|
|
|
213
|
-
async function runForeground(values) {
|
|
214
|
-
const { port, provider, app } = resolve(values);
|
|
215
|
-
if (provider === 'tailscale' || provider === 'ts') return runTailscaleForeground(values, port);
|
|
216
|
-
const log = createFileLogger(bridgeLogPath());
|
|
217
|
-
const extra = { provider };
|
|
218
|
-
if (provider === 'relay') {
|
|
219
|
-
const relay = ensureRelayConfig(app);
|
|
220
|
-
extra.relayUrl = relay.relayUrl;
|
|
221
|
-
extra.token = relay.token;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Senal de apagado: el supervisor de abajo la consulta entre intentos.
|
|
225
|
-
let interrupted = false;
|
|
226
|
-
let notifyInterrupt = null;
|
|
227
|
-
const interruptPromise = new Promise((r) => {
|
|
228
|
-
notifyInterrupt = r;
|
|
229
|
-
});
|
|
230
|
-
const shutdown = () => {
|
|
231
|
-
interrupted = true;
|
|
232
|
-
notifyInterrupt();
|
|
233
|
-
};
|
|
234
|
-
process.once('SIGINT', shutdown);
|
|
235
|
-
process.once('SIGTERM', shutdown);
|
|
236
|
-
const sleep = (ms) =>
|
|
237
|
-
new Promise((r) => {
|
|
238
|
-
const t = setTimeout(r, ms);
|
|
239
|
-
interruptPromise.then(() => {
|
|
240
|
-
clearTimeout(t);
|
|
241
|
-
r();
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Supervision: si el proceso del tunel muere (el servicio gratuito lo hace a
|
|
246
|
-
// menudo), reconecta con backoff y reescribe el estado con la URL nueva.
|
|
247
|
-
let attempt = 0;
|
|
248
|
-
let tun;
|
|
249
|
-
for (;;) {
|
|
250
|
-
try {
|
|
251
|
-
tun = await startTunnel(port, provider, { log, verbose: values.verbose, ...extra });
|
|
252
|
-
} catch (err) {
|
|
253
|
-
if (interrupted) break;
|
|
254
|
-
log(`ERROR: ${err.message}`);
|
|
255
|
-
return 1;
|
|
256
|
-
}
|
|
257
|
-
if (!tun.url) {
|
|
258
|
-
log('ERROR: el tunel no devolvio URL.');
|
|
259
|
-
return 1;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
writeBridgeState({
|
|
263
|
-
pid: process.pid,
|
|
264
|
-
url: tun.url,
|
|
265
|
-
publicUrl: provider === 'relay' ? relayConsoleUrl(extra) : undefined,
|
|
266
|
-
provider: tun.provider,
|
|
267
|
-
port,
|
|
268
|
-
childPid: tun.pid,
|
|
269
|
-
startedAt: new Date().toISOString(),
|
|
270
|
-
});
|
|
271
|
-
printStatus(readBridgeState(), true);
|
|
272
|
-
log(`Tunel publico: ${tun.url}`);
|
|
273
|
-
attempt = 0;
|
|
274
|
-
|
|
275
|
-
const exitCode = await new Promise((r) => {
|
|
276
|
-
// Health-check cada 60 s: si la URL publica deja de responder dos veces
|
|
277
|
-
// seguidas, matamos el tunel y el supervisor abre otro.
|
|
278
|
-
let fails = 0;
|
|
279
|
-
const probe = async () => {
|
|
280
|
-
const probeUrl = tun.probeUrl ?? `${tun.url}/api/health`;
|
|
281
|
-
try {
|
|
282
|
-
const res = await fetch(probeUrl, { signal: AbortSignal.timeout(8000) });
|
|
283
|
-
if (res.ok) fails = 0;
|
|
284
|
-
else fails += 1;
|
|
285
|
-
} catch {
|
|
286
|
-
fails += 1;
|
|
287
|
-
}
|
|
288
|
-
if (fails >= 2) {
|
|
289
|
-
log(`La URL publica no responde (${tun.url}); reiniciando el tunel...`);
|
|
290
|
-
tun.stop();
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
const health = setInterval(probe, 60000);
|
|
294
|
-
health.unref?.();
|
|
295
|
-
tun.child?.once('exit', (code) => {
|
|
296
|
-
clearInterval(health);
|
|
297
|
-
r(code ?? 1);
|
|
298
|
-
});
|
|
299
|
-
if (!tun.child) {
|
|
300
|
-
clearInterval(health);
|
|
301
|
-
r(0);
|
|
302
|
-
}
|
|
303
|
-
interruptPromise.then(() => {
|
|
304
|
-
clearInterval(health);
|
|
305
|
-
r('signal');
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
if (interrupted || exitCode === 'signal') break;
|
|
310
|
-
attempt += 1;
|
|
311
|
-
const wait = Math.min(30000, 2000 * attempt);
|
|
312
|
-
log(`El tunel termino (codigo ${exitCode}); reconectando en ${Math.round(wait / 1000)} s...`);
|
|
313
|
-
await sleep(wait);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
tun?.stop?.();
|
|
318
|
-
} catch {
|
|
319
|
-
/* nada */
|
|
320
|
-
}
|
|
321
|
-
clearBridgeState();
|
|
322
|
-
return 0;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
177
|
async function cmdReload(values) {
|
|
326
178
|
// Leer el estado ANTES de parar: clearBridgeState borra el archivo y despues
|
|
327
|
-
// no habria nada que esperar
|
|
179
|
+
// no habria nada que esperar.
|
|
328
180
|
const previous = readBridgeState();
|
|
329
181
|
await cmdStop();
|
|
330
182
|
for (let i = 0; i < 25 && previous?.pid && pidAlive(previous.pid); i++) {
|
|
@@ -336,29 +188,22 @@ async function cmdReload(values) {
|
|
|
336
188
|
async function cmdStop() {
|
|
337
189
|
const state = readBridgeState();
|
|
338
190
|
const stopped = [];
|
|
339
|
-
if (state
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
191
|
+
if (!state) {
|
|
192
|
+
console.log('El bridge no esta corriendo.');
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await resetTailscaleServe();
|
|
197
|
+
stopped.push('tailscale serve');
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.log(`No pude ejecutar tailscale serve reset: ${error.message}`);
|
|
346
200
|
}
|
|
347
|
-
// Mata el supervisor
|
|
348
|
-
|
|
349
|
-
if (state?.pid && pidAlive(state.pid)) {
|
|
201
|
+
// Mata el supervisor si quedo vivo.
|
|
202
|
+
if (state.pid && pidAlive(state.pid)) {
|
|
350
203
|
killPid(state.pid);
|
|
351
204
|
stopped.push(`bridge ${state.pid}`);
|
|
352
205
|
}
|
|
353
|
-
if (state?.childPid && pidAlive(state.childPid)) {
|
|
354
|
-
killPid(state.childPid);
|
|
355
|
-
stopped.push(`tunel ${state.childPid}`);
|
|
356
|
-
}
|
|
357
206
|
clearBridgeState();
|
|
358
|
-
if (!stopped.length) {
|
|
359
|
-
console.log('El bridge no esta corriendo.');
|
|
360
|
-
return 0;
|
|
361
|
-
}
|
|
362
207
|
console.log(`Bridge detenido (${stopped.join(', ')}).`);
|
|
363
208
|
return 0;
|
|
364
209
|
}
|
|
@@ -374,18 +219,17 @@ async function cmdStatus() {
|
|
|
374
219
|
}
|
|
375
220
|
|
|
376
221
|
function printStatus(state, withQr = false) {
|
|
377
|
-
const shown = state.publicUrl || state.url;
|
|
378
222
|
console.log(`Bridge corriendo (pid ${state.pid})`);
|
|
379
|
-
console.log(` url : ${
|
|
223
|
+
console.log(` url : ${state.url}`);
|
|
380
224
|
console.log(` proveedor: ${state.provider} puerto local: ${state.port}`);
|
|
381
|
-
|
|
225
|
+
console.log(' red : privada (tailnet; solo tus dispositivos)');
|
|
382
226
|
console.log(` casa : ${homeDir()}`);
|
|
383
227
|
if (withQr && state.url) {
|
|
384
|
-
const art = qrTerminal(
|
|
228
|
+
const art = qrTerminal(state.url, 2);
|
|
385
229
|
if (art) {
|
|
386
230
|
console.log('');
|
|
387
231
|
console.log(art);
|
|
388
|
-
console.log(` escanea para abrir ${
|
|
232
|
+
console.log(` escanea para abrir ${state.url}`);
|
|
389
233
|
}
|
|
390
234
|
}
|
|
391
235
|
}
|
|
@@ -393,23 +237,20 @@ function printStatus(state, withQr = false) {
|
|
|
393
237
|
function printHelp() {
|
|
394
238
|
console.log(`Uso: tamnora bridge [accion] [opciones]
|
|
395
239
|
|
|
396
|
-
Da acceso remoto al server local y muestra la URL + QR.
|
|
240
|
+
Da acceso remoto al server local con Tailscale (VPN privada) y muestra la URL + QR.
|
|
397
241
|
|
|
398
242
|
Acciones:
|
|
399
243
|
--start Abre el acceso en segundo plano + QR (default)
|
|
400
244
|
--reload Reinicia
|
|
401
|
-
--stop Cierra
|
|
245
|
+
--stop Cierra (tailscale serve reset)
|
|
402
246
|
--status Estado y URL
|
|
403
247
|
--foreground Corre en primer plano (debug)
|
|
404
248
|
|
|
405
249
|
Opciones:
|
|
406
|
-
--
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
relay mailbox propio en harness.tamnora.com (legacy*)
|
|
410
|
-
|
|
411
|
-
Tailscale requiere instalarlo y estar logueado (https://tailscale.com/download).
|
|
250
|
+
--port <n> Puerto local a exponer (default: el del server)
|
|
251
|
+
--home <path> Casa de Tamnora (default: TAMNORA_HOME o datos del sistema)
|
|
252
|
+
--insecure Permitir el acceso sin usuario configurado (peligroso)
|
|
412
253
|
|
|
413
|
-
|
|
414
|
-
|
|
254
|
+
Requiere Tailscale instalado y logueado (https://tailscale.com/download) en la PC
|
|
255
|
+
y en el dispositivo remoto.`);
|
|
415
256
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { parseTunnelmoleUrls, parseTailscaleStatus } from '../src/lib/tunnel.js';
|
|
3
|
+
import { parseTunnelmoleUrls, parseTailscaleStatus, startTunnel } from '../src/lib/tunnel.js';
|
|
4
4
|
import { qrTerminal } from '../src/lib/qr.js';
|
|
5
5
|
|
|
6
6
|
test('parseTunnelmoleUrls extracts the https URL', () => {
|
|
@@ -28,6 +28,10 @@ test('parseTailscaleStatus marca desconectado sin DNSName', () => {
|
|
|
28
28
|
assert.equal(out.dnsName, '');
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
test('startTunnel rechaza el proveedor retirado (relay)', async () => {
|
|
32
|
+
await assert.rejects(() => startTunnel(4517, 'relay'), /no soportado/);
|
|
33
|
+
});
|
|
34
|
+
|
|
31
35
|
test('qrTerminal renders a block matrix for a URL', () => {
|
|
32
36
|
const art = qrTerminal('https://abc-123.tunnelmole.net', 1);
|
|
33
37
|
assert.ok(art.length > 0);
|
|
@@ -29,12 +29,10 @@ export function readApp() {
|
|
|
29
29
|
}
|
|
30
30
|
const app = { ...DEFAULT_APP, ...(data && typeof data === 'object' ? data : {}) };
|
|
31
31
|
app.tunnel = { ...DEFAULT_APP.tunnel, ...(app.tunnel || {}) };
|
|
32
|
-
// tunnelmole fue el default viejo y
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
if (app.tunnel.provider
|
|
36
|
-
app.tunnel.provider = 'tailscale';
|
|
37
|
-
}
|
|
32
|
+
// `tunnelmole` fue el default viejo y `relay` fue un proveedor retirado: lo
|
|
33
|
+
// persistido en instalaciones previas se migra a tailscale.
|
|
34
|
+
const legacy = ['tunnelmole', 'tmole', 'relay'];
|
|
35
|
+
if (legacy.includes(app.tunnel.provider)) app.tunnel.provider = 'tailscale';
|
|
38
36
|
return app;
|
|
39
37
|
}
|
|
40
38
|
|