@danieltmn/tamnora 0.9.6 → 0.11.0
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 +29 -0
- package/README.md +19 -8
- package/SECURITY.md +10 -7
- package/harness/packages/cli/src/cli.js +1 -1
- package/harness/packages/cli/src/commands/bridge.js +87 -166
- package/harness/packages/cli/test/tunnel.test.js +20 -1
- package/harness/packages/core/src/config/app.js +5 -1
- package/harness/packages/core/src/index.js +11 -1
- package/harness/packages/core/src/tunnel.js +105 -135
- package/harness/packages/core/test/home.test.js +11 -0
- package/harness/packages/server/public/app.js +3 -30
- package/harness/packages/server/public/index.html +1 -1
- package/harness/packages/server/test/security.test.js +15 -0
- 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,35 @@ 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.0] - 2026-10-09
|
|
9
|
+
|
|
10
|
+
### Quitado
|
|
11
|
+
|
|
12
|
+
- **Tuneles publicos retirados**: `tamnora bridge` ahora soporta unicamente
|
|
13
|
+
**tailscale**. Se elimino el relay propio (`relay/index.php`, el deploy FTPS
|
|
14
|
+
`scripts/deploy-relay.mjs` y el transporte `relayClient.js` de la consola) y el
|
|
15
|
+
proveedor `tunnelmole` dejo de ofrecerse para el acceso remoto (se conserva solo
|
|
16
|
+
para el feature de `preview`). Un `app.json` con `tunnel.provider` en
|
|
17
|
+
`tunnelmole`/`relay` se migra a `tailscale` al leerlo.
|
|
18
|
+
|
|
19
|
+
## [0.10.0] - 2026-10-09
|
|
20
|
+
|
|
21
|
+
### Agregado
|
|
22
|
+
|
|
23
|
+
- **Acceso remoto con Tailscale**: nuevo proveedor `tailscale` para `tamnora
|
|
24
|
+
bridge` (default). Levanta `tailscale serve` y publica HTTPS dentro del tailnet
|
|
25
|
+
reenviando a loopback, asi que la velocidad es casi local, no hay exposicion
|
|
26
|
+
publica y la consola + tools funcionan como en local (una sola base, sin sync).
|
|
27
|
+
`tunnelState()` ignora este proveedor para que los guards locales sigan
|
|
28
|
+
confiando; `bridge --start/--stop/--status` y el QR lo soportan.
|
|
29
|
+
|
|
30
|
+
### Obsoleto
|
|
31
|
+
|
|
32
|
+
- Los tuneles publicos `tunnelmole` y `relay` quedan como **legacy** (`bridge`
|
|
33
|
+
avisa al usarlos) y **se retiran en la proxima version**. Hasta entonces siguen
|
|
34
|
+
funcionales; el relay (`harness.tamnora.com`) continua online. `tunnelmole` se
|
|
35
|
+
conserva solo para el feature de `preview`.
|
|
36
|
+
|
|
8
37
|
## [0.9.6] - 2026-10-08
|
|
9
38
|
|
|
10
39
|
### Cambios
|
package/README.md
CHANGED
|
@@ -144,17 +144,28 @@ corre en modo local sin login. Al cambiar el usuario se detiene el server
|
|
|
144
144
|
|
|
145
145
|
### Acceso remoto
|
|
146
146
|
|
|
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).
|
|
151
|
+
|
|
147
152
|
```bash
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
tamnora
|
|
151
|
-
|
|
153
|
+
# 1. Instalar Tailscale en esta PC y en el dispositivo remoto (misma cuenta)
|
|
154
|
+
# https://tailscale.com/download
|
|
155
|
+
tamnora user --name yo --password unaClaveLarga
|
|
156
|
+
|
|
157
|
+
# 2. Server + acceso remoto
|
|
158
|
+
tamnora bridge --start # tailscale serve (URL + QR)
|
|
159
|
+
tamnora bridge --status # https://<pc>.<tailnet>.ts.net
|
|
160
|
+
tamnora bridge --stop # tailscale serve reset
|
|
152
161
|
```
|
|
153
162
|
|
|
154
|
-
`tamnora bridge`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
`tamnora bridge --start` muestra la URL y un **QR** para abrirlo desde el celu.
|
|
164
|
+
Sigue protegido por el login del server: configura un usuario antes de exponerlo
|
|
165
|
+
(ver [`SECURITY.md`](SECURITY.md)).
|
|
166
|
+
|
|
167
|
+
Los tuneles publicos (tunnelmole, relay) fueron **retirados**; `tunnelmole` se
|
|
168
|
+
conserva solo para el feature de *preview*.
|
|
158
169
|
|
|
159
170
|
### Telegram (canales de mensajeria)
|
|
160
171
|
|
package/SECURITY.md
CHANGED
|
@@ -14,13 +14,16 @@ asi que se lo debe tratar como codigo con acceso a tu usuario.
|
|
|
14
14
|
|
|
15
15
|
- **Local (loopback)**: quien ya tiene tu sesion de usuario ya puede correr
|
|
16
16
|
comandos; el server no agrega una barrera adicional mas alla del login.
|
|
17
|
-
- **Acceso remoto por
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
- **Acceso remoto por Tailscale (recomendado)**: `tailscale serve` publica el
|
|
18
|
+
server **solo dentro de tu tailnet** y reenvia por loopback. No hay exposicion
|
|
19
|
+
publica; quien accede es uno de tus dispositivos. Aun asi configura un
|
|
20
|
+
**usuario** con contrasena fuerte (defensa en profundidad). Como entra por
|
|
21
|
+
loopback, una vez autenticado se alcanza toda la consola y las tools
|
|
22
|
+
(explorador de archivos, shell, git): tratalo como acceso completo a tu PC.
|
|
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.
|
|
24
27
|
- **Credenciales**: las API keys de proveedores se guardan en la base SQLite de
|
|
25
28
|
la casa local (fuera del repo) **en texto plano**; protege el archivo con los
|
|
26
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,
|
|
@@ -18,7 +11,7 @@ import {
|
|
|
18
11
|
tailLines,
|
|
19
12
|
bridgeLogPath,
|
|
20
13
|
} from '../lib/daemon.js';
|
|
21
|
-
import { startTunnel } from '../lib/tunnel.js';
|
|
14
|
+
import { startTunnel, probeTailscale, resetTailscaleServe } from '../lib/tunnel.js';
|
|
22
15
|
import { qrTerminal } from '../lib/qr.js';
|
|
23
16
|
|
|
24
17
|
const OPTIONS = {
|
|
@@ -40,6 +33,8 @@ const readBridgeState = () => bridgeState.read();
|
|
|
40
33
|
const writeBridgeState = (data) => bridgeState.write(data);
|
|
41
34
|
const clearBridgeState = () => bridgeState.clear();
|
|
42
35
|
|
|
36
|
+
const isTailscale = (provider) => provider === 'tailscale' || provider === 'ts';
|
|
37
|
+
|
|
43
38
|
export async function bridgeCommand(argv) {
|
|
44
39
|
const { values } = parse(argv, OPTIONS);
|
|
45
40
|
if (values.help) {
|
|
@@ -62,47 +57,42 @@ export async function bridgeCommand(argv) {
|
|
|
62
57
|
function resolve(values) {
|
|
63
58
|
ensureHome();
|
|
64
59
|
const app = ensureApp();
|
|
60
|
+
const requested = values.tunnel ? String(values.tunnel).toLowerCase() : '';
|
|
65
61
|
return {
|
|
66
62
|
port: num(values.port, app.port || 4517),
|
|
67
|
-
provider:
|
|
63
|
+
provider: requested || String(app.tunnel?.provider || 'tailscale').toLowerCase(),
|
|
68
64
|
app,
|
|
69
65
|
};
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
/** Asegura la configuracion del relay (url + token) en app.json y la devuelve. */
|
|
73
|
-
function ensureRelayConfig(app) {
|
|
74
|
-
const tunnel = { ...app.tunnel };
|
|
75
|
-
if (!tunnel.relayUrl) {
|
|
76
|
-
tunnel.relayUrl = process.env.TAMNORA_RELAY_URL || 'https://harness.tamnora.com/index.php';
|
|
77
|
-
}
|
|
78
|
-
if (!tunnel.token) {
|
|
79
|
-
tunnel.token = process.env.TAMNORA_RELAY_TOKEN || randomToken(24);
|
|
80
|
-
const current = readApp();
|
|
81
|
-
writeApp({ ...current, tunnel: { ...current.tunnel, token: tunnel.token, relayUrl: tunnel.relayUrl } });
|
|
82
|
-
console.log(`Token de relay nuevo (guardado en app.json): ${tunnel.token}`);
|
|
83
|
-
}
|
|
84
|
-
return tunnel;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
68
|
async function cmdStart(values) {
|
|
88
69
|
const { port, provider } = resolve(values);
|
|
89
70
|
if (provider === 'none') {
|
|
90
|
-
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.');
|
|
91
77
|
return 1;
|
|
92
78
|
}
|
|
93
79
|
|
|
94
|
-
//
|
|
95
|
-
// (incluido /api/chat con tool bash): hay que pedirlo explicito.
|
|
80
|
+
// Por la red privada igual conviene un usuario: defensa en profundidad.
|
|
96
81
|
const app = ensureApp();
|
|
97
82
|
if (!app.user?.password && !values.insecure) {
|
|
98
|
-
console.error('No hay usuario configurado:
|
|
83
|
+
console.error('No hay usuario configurado: sin login cualquiera en tu tailnet tendria acceso total.');
|
|
99
84
|
console.error('Crea uno con: tamnora user --name <usuario> --password <clave>');
|
|
100
85
|
console.error('Si igual queres abrirlo sin proteccion: tamnora bridge --insecure');
|
|
101
86
|
return 1;
|
|
102
87
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
|
|
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;
|
|
106
96
|
}
|
|
107
97
|
|
|
108
98
|
const current = readBridgeState();
|
|
@@ -113,11 +103,11 @@ async function cmdStart(values) {
|
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
const bin = process.argv[1];
|
|
116
|
-
const args = [bin, 'bridge', '--foreground', '--port', String(port)
|
|
106
|
+
const args = [bin, 'bridge', '--foreground', '--port', String(port)];
|
|
117
107
|
if (process.env.TAMNORA_HOME) args.push('--home', process.env.TAMNORA_HOME);
|
|
118
108
|
if (values.verbose) args.push('--verbose');
|
|
119
109
|
const { child, isDead } = spawnDetached(args, { envFlag: 'TAMNORA_BRIDGE_CHILD' });
|
|
120
|
-
console.log(`Abriendo
|
|
110
|
+
console.log(`Abriendo acceso remoto (tailscale) en el puerto ${port}...`);
|
|
121
111
|
|
|
122
112
|
const state = await waitForState(() => readBridgeState(), {
|
|
123
113
|
childPid: child.pid,
|
|
@@ -128,123 +118,55 @@ async function cmdStart(values) {
|
|
|
128
118
|
if (!state) {
|
|
129
119
|
const tail = tailLines(bridgeLogPath(), 8);
|
|
130
120
|
if (tail) console.log(tail);
|
|
131
|
-
console.log('No pude abrir el
|
|
121
|
+
console.log('No pude abrir el acceso remoto. Revisa: tamnora bridge --status');
|
|
132
122
|
return 1;
|
|
133
123
|
}
|
|
134
124
|
printStatus(state, true);
|
|
135
125
|
return 0;
|
|
136
126
|
}
|
|
137
127
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Tailscale es configuracion persistente (`tailscale serve`), no un proceso:
|
|
130
|
+
* no hay health-check ni bucle de reconexion, solo esperar la senal de apagado.
|
|
131
|
+
*/
|
|
144
132
|
async function runForeground(values) {
|
|
145
|
-
const { port, provider
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const relay = ensureRelayConfig(app);
|
|
150
|
-
extra.relayUrl = relay.relayUrl;
|
|
151
|
-
extra.token = relay.token;
|
|
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;
|
|
152
137
|
}
|
|
153
|
-
|
|
154
|
-
// Senal de apagado: el supervisor de abajo la consulta entre intentos.
|
|
155
|
-
let interrupted = false;
|
|
156
|
-
let notifyInterrupt = null;
|
|
157
|
-
const interruptPromise = new Promise((r) => {
|
|
158
|
-
notifyInterrupt = r;
|
|
159
|
-
});
|
|
160
|
-
const shutdown = () => {
|
|
161
|
-
interrupted = true;
|
|
162
|
-
notifyInterrupt();
|
|
163
|
-
};
|
|
164
|
-
process.once('SIGINT', shutdown);
|
|
165
|
-
process.once('SIGTERM', shutdown);
|
|
166
|
-
const sleep = (ms) =>
|
|
167
|
-
new Promise((r) => {
|
|
168
|
-
const t = setTimeout(r, ms);
|
|
169
|
-
interruptPromise.then(() => {
|
|
170
|
-
clearTimeout(t);
|
|
171
|
-
r();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Supervision: si el proceso del tunel muere (el servicio gratuito lo hace a
|
|
176
|
-
// menudo), reconecta con backoff y reescribe el estado con la URL nueva.
|
|
177
|
-
let attempt = 0;
|
|
138
|
+
const log = createFileLogger(bridgeLogPath());
|
|
178
139
|
let tun;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return 1;
|
|
186
|
-
}
|
|
187
|
-
if (!tun.url) {
|
|
188
|
-
log('ERROR: el tunel no devolvio URL.');
|
|
189
|
-
return 1;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
writeBridgeState({
|
|
193
|
-
pid: process.pid,
|
|
194
|
-
url: tun.url,
|
|
195
|
-
publicUrl: provider === 'relay' ? relayConsoleUrl(extra) : undefined,
|
|
196
|
-
provider: tun.provider,
|
|
197
|
-
port,
|
|
198
|
-
childPid: tun.pid,
|
|
199
|
-
startedAt: new Date().toISOString(),
|
|
200
|
-
});
|
|
201
|
-
printStatus(readBridgeState(), true);
|
|
202
|
-
log(`Tunel publico: ${tun.url}`);
|
|
203
|
-
attempt = 0;
|
|
204
|
-
|
|
205
|
-
const exitCode = await new Promise((r) => {
|
|
206
|
-
// Health-check cada 60 s: si la URL publica deja de responder dos veces
|
|
207
|
-
// seguidas, matamos el tunel y el supervisor abre otro.
|
|
208
|
-
let fails = 0;
|
|
209
|
-
const probe = async () => {
|
|
210
|
-
const probeUrl = tun.probeUrl ?? `${tun.url}/api/health`;
|
|
211
|
-
try {
|
|
212
|
-
const res = await fetch(probeUrl, { signal: AbortSignal.timeout(8000) });
|
|
213
|
-
if (res.ok) fails = 0;
|
|
214
|
-
else fails += 1;
|
|
215
|
-
} catch {
|
|
216
|
-
fails += 1;
|
|
217
|
-
}
|
|
218
|
-
if (fails >= 2) {
|
|
219
|
-
log(`La URL publica no responde (${tun.url}); reiniciando el tunel...`);
|
|
220
|
-
tun.stop();
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
const health = setInterval(probe, 60000);
|
|
224
|
-
health.unref?.();
|
|
225
|
-
tun.child?.once('exit', (code) => {
|
|
226
|
-
clearInterval(health);
|
|
227
|
-
r(code ?? 1);
|
|
228
|
-
});
|
|
229
|
-
if (!tun.child) {
|
|
230
|
-
clearInterval(health);
|
|
231
|
-
r(0);
|
|
232
|
-
}
|
|
233
|
-
interruptPromise.then(() => {
|
|
234
|
-
clearInterval(health);
|
|
235
|
-
r('signal');
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
if (interrupted || exitCode === 'signal') break;
|
|
240
|
-
attempt += 1;
|
|
241
|
-
const wait = Math.min(30000, 2000 * attempt);
|
|
242
|
-
log(`El tunel termino (codigo ${exitCode}); reconectando en ${Math.round(wait / 1000)} s...`);
|
|
243
|
-
await sleep(wait);
|
|
140
|
+
try {
|
|
141
|
+
tun = await startTunnel(port, 'tailscale', { log, verbose: values.verbose });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
log(`ERROR: ${error.message}`);
|
|
144
|
+
console.error(error.message);
|
|
145
|
+
return 1;
|
|
244
146
|
}
|
|
245
|
-
|
|
147
|
+
writeBridgeState({
|
|
148
|
+
pid: process.pid,
|
|
149
|
+
url: tun.url,
|
|
150
|
+
provider: 'tailscale',
|
|
151
|
+
port,
|
|
152
|
+
childPid: 0,
|
|
153
|
+
startedAt: new Date().toISOString(),
|
|
154
|
+
});
|
|
155
|
+
printStatus(readBridgeState(), true);
|
|
156
|
+
log(`Tailscale: ${tun.url}`);
|
|
157
|
+
// Sin proceso hijo ni timers, el event loop quedaria vacio y node saldria:
|
|
158
|
+
// un timer referenced mantiene vivo al supervisor hasta la senal de apagado.
|
|
159
|
+
await new Promise((resolve) => {
|
|
160
|
+
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
161
|
+
const stop = () => {
|
|
162
|
+
clearInterval(keepAlive);
|
|
163
|
+
resolve();
|
|
164
|
+
};
|
|
165
|
+
process.once('SIGINT', stop);
|
|
166
|
+
process.once('SIGTERM', stop);
|
|
167
|
+
});
|
|
246
168
|
try {
|
|
247
|
-
tun
|
|
169
|
+
await tun.stop?.();
|
|
248
170
|
} catch {
|
|
249
171
|
/* nada */
|
|
250
172
|
}
|
|
@@ -254,7 +176,7 @@ async function runForeground(values) {
|
|
|
254
176
|
|
|
255
177
|
async function cmdReload(values) {
|
|
256
178
|
// Leer el estado ANTES de parar: clearBridgeState borra el archivo y despues
|
|
257
|
-
// no habria nada que esperar
|
|
179
|
+
// no habria nada que esperar.
|
|
258
180
|
const previous = readBridgeState();
|
|
259
181
|
await cmdStop();
|
|
260
182
|
for (let i = 0; i < 25 && previous?.pid && pidAlive(previous.pid); i++) {
|
|
@@ -266,21 +188,22 @@ async function cmdReload(values) {
|
|
|
266
188
|
async function cmdStop() {
|
|
267
189
|
const state = readBridgeState();
|
|
268
190
|
const stopped = [];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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}`);
|
|
200
|
+
}
|
|
201
|
+
// Mata el supervisor si quedo vivo.
|
|
202
|
+
if (state.pid && pidAlive(state.pid)) {
|
|
272
203
|
killPid(state.pid);
|
|
273
204
|
stopped.push(`bridge ${state.pid}`);
|
|
274
205
|
}
|
|
275
|
-
if (state?.childPid && pidAlive(state.childPid)) {
|
|
276
|
-
killPid(state.childPid);
|
|
277
|
-
stopped.push(`tunel ${state.childPid}`);
|
|
278
|
-
}
|
|
279
206
|
clearBridgeState();
|
|
280
|
-
if (!stopped.length) {
|
|
281
|
-
console.log('El bridge no esta corriendo.');
|
|
282
|
-
return 0;
|
|
283
|
-
}
|
|
284
207
|
console.log(`Bridge detenido (${stopped.join(', ')}).`);
|
|
285
208
|
return 0;
|
|
286
209
|
}
|
|
@@ -296,17 +219,17 @@ async function cmdStatus() {
|
|
|
296
219
|
}
|
|
297
220
|
|
|
298
221
|
function printStatus(state, withQr = false) {
|
|
299
|
-
const shown = state.publicUrl || state.url;
|
|
300
222
|
console.log(`Bridge corriendo (pid ${state.pid})`);
|
|
301
|
-
console.log(` url : ${
|
|
223
|
+
console.log(` url : ${state.url}`);
|
|
302
224
|
console.log(` proveedor: ${state.provider} puerto local: ${state.port}`);
|
|
225
|
+
console.log(' red : privada (tailnet; solo tus dispositivos)');
|
|
303
226
|
console.log(` casa : ${homeDir()}`);
|
|
304
227
|
if (withQr && state.url) {
|
|
305
|
-
const art = qrTerminal(
|
|
228
|
+
const art = qrTerminal(state.url, 2);
|
|
306
229
|
if (art) {
|
|
307
230
|
console.log('');
|
|
308
231
|
console.log(art);
|
|
309
|
-
console.log(` escanea para abrir ${
|
|
232
|
+
console.log(` escanea para abrir ${state.url}`);
|
|
310
233
|
}
|
|
311
234
|
}
|
|
312
235
|
}
|
|
@@ -314,22 +237,20 @@ function printStatus(state, withQr = false) {
|
|
|
314
237
|
function printHelp() {
|
|
315
238
|
console.log(`Uso: tamnora bridge [accion] [opciones]
|
|
316
239
|
|
|
317
|
-
|
|
240
|
+
Da acceso remoto al server local con Tailscale (VPN privada) y muestra la URL + QR.
|
|
318
241
|
|
|
319
242
|
Acciones:
|
|
320
|
-
--start Abre el
|
|
321
|
-
--reload Reinicia
|
|
322
|
-
--stop Cierra
|
|
323
|
-
--status Estado y URL
|
|
243
|
+
--start Abre el acceso en segundo plano + QR (default)
|
|
244
|
+
--reload Reinicia
|
|
245
|
+
--stop Cierra (tailscale serve reset)
|
|
246
|
+
--status Estado y URL
|
|
324
247
|
--foreground Corre en primer plano (debug)
|
|
325
248
|
|
|
326
249
|
Opciones:
|
|
327
|
-
--tunnel <prov> Proveedor (default: el de app.json; hoy: tunnelmole o relay)
|
|
328
250
|
--port <n> Puerto local a exponer (default: el del server)
|
|
329
251
|
--home <path> Casa de Tamnora (default: TAMNORA_HOME o datos del sistema)
|
|
330
|
-
--insecure Permitir el
|
|
252
|
+
--insecure Permitir el acceso sin usuario configurado (peligroso)
|
|
331
253
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
token ya puesto.`);
|
|
254
|
+
Requiere Tailscale instalado y logueado (https://tailscale.com/download) en la PC
|
|
255
|
+
y en el dispositivo remoto.`);
|
|
335
256
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { parseTunnelmoleUrls } 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', () => {
|
|
@@ -13,6 +13,25 @@ test('parseTunnelmoleUrls ignores unrelated output', () => {
|
|
|
13
13
|
assert.deepEqual(parseTunnelmoleUrls('starting tunnelmole...'), { https: '', http: '' });
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
test('parseTailscaleStatus lee el DNSName del nodo y su IPv4', () => {
|
|
17
|
+
const out = parseTailscaleStatus(
|
|
18
|
+
JSON.stringify({ Self: { DNSName: 'pc.tailnet.ts.net.', TailscaleIPs: ['100.64.0.5', 'fd7a:115c:a1e0::5'] } })
|
|
19
|
+
);
|
|
20
|
+
assert.equal(out.dnsName, 'pc.tailnet.ts.net');
|
|
21
|
+
assert.equal(out.ipv4, '100.64.0.5');
|
|
22
|
+
assert.ok(out.connected);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('parseTailscaleStatus marca desconectado sin DNSName', () => {
|
|
26
|
+
const out = parseTailscaleStatus(JSON.stringify({ Self: { TailscaleIPs: [] } }));
|
|
27
|
+
assert.equal(out.connected, false);
|
|
28
|
+
assert.equal(out.dnsName, '');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('startTunnel rechaza el proveedor retirado (relay)', async () => {
|
|
32
|
+
await assert.rejects(() => startTunnel(4517, 'relay'), /no soportado/);
|
|
33
|
+
});
|
|
34
|
+
|
|
16
35
|
test('qrTerminal renders a block matrix for a URL', () => {
|
|
17
36
|
const art = qrTerminal('https://abc-123.tunnelmole.net', 1);
|
|
18
37
|
assert.ok(art.length > 0);
|
|
@@ -9,7 +9,7 @@ import { appConfigPath, ensureHome } from './home.js';
|
|
|
9
9
|
export const DEFAULT_APP = {
|
|
10
10
|
port: 4517,
|
|
11
11
|
host: '127.0.0.1',
|
|
12
|
-
tunnel: { provider: '
|
|
12
|
+
tunnel: { provider: 'tailscale', domain: '' },
|
|
13
13
|
authSecret: '',
|
|
14
14
|
user: null,
|
|
15
15
|
// Datos del usuario para que el agente lo conozca (se inyecta en el prompt).
|
|
@@ -29,6 +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 `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';
|
|
32
36
|
return app;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -94,7 +94,17 @@ export {
|
|
|
94
94
|
registryServerToConfig,
|
|
95
95
|
normalizeMcpConfig,
|
|
96
96
|
} from './mcp/registry.js';
|
|
97
|
-
export {
|
|
97
|
+
export {
|
|
98
|
+
parseTunnelmoleUrls,
|
|
99
|
+
parseTailscaleStatus,
|
|
100
|
+
probeTailscale,
|
|
101
|
+
resetTailscaleServe,
|
|
102
|
+
startTailscaleServe,
|
|
103
|
+
startTunnelmole,
|
|
104
|
+
startTunnel,
|
|
105
|
+
tunnelState,
|
|
106
|
+
tunnelActive,
|
|
107
|
+
} from './tunnel.js';
|
|
98
108
|
export {
|
|
99
109
|
TRUSTED_CLIENT_TOKEN,
|
|
100
110
|
EDGE_VERSION,
|