@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 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
- tamnora bridge --start # abre un tunel publico (tunnelmole) + QR
149
- tamnora bridge --status # URL publica
150
- tamnora bridge --reload # reinicia el tunel
151
- tamnora bridge --stop
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` abre un tunel (tunnelmole) hacia tu server local y muestra la
155
- URL publica **y un QR** para abrirlo desde el celular. Trabaja protegido por el
156
- login del server: configura un usuario antes de exponerlo (ver
157
- [`SECURITY.md`](SECURITY.md)).
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 tunel (tunnelmole)**: la URL publica es accesible desde
18
- Internet. Configura un **usuario** (asi toda `/api/*` pide sesion) con una
19
- **contrasena fuerte**; nunca expongas un server sin login. El tunnel reenvia
20
- por loopback, de modo que una vez autenticado se alcanza toda la consola
21
- (incluidos el explorador de archivos y el selector de carpetas): tratalo como
22
- acceso completo a tu maquina. Cierra el tunnel (`tamnora bridge --stop`) cuando
23
- no lo uses.
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] Tunel publico (tunnelmole) + QR
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: String(values.tunnel || app.tunnel?.provider || 'tunnelmole').toLowerCase(),
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 tunel esta desactivado (provider "none"). Usa --tunnel tunnelmole.');
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
- // Exponer el server a Internet sin usuario configurado deja /api/* abierto
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: por el tunel cualquiera con la URL tendria acceso total.');
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
- if (provider === 'tunnelmole' || provider === 'tmole') {
104
- console.log('Aviso: tunnelmole gratuito admite una sola conexion; si abriste el');
105
- console.log('preview de la consola, cerralo (o el bridge) antes de usar el otro.');
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), '--tunnel', provider];
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 tunel (${provider}) en el puerto ${port}...`);
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 tunel. Revisa: tamnora bridge --status');
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
- /** URL publica para el celular: la consola agregada al relay con el token. */
139
- function relayConsoleUrl(extra) {
140
- const base = String(extra.relayUrl ?? '').replace(/\/+$/, '').replace(/index\.php$/, '').replace(/\/$/, '');
141
- return `${base}/console/?t=${extra.token}`;
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, app } = resolve(values);
146
- const log = createFileLogger(bridgeLogPath());
147
- const extra = { provider };
148
- if (provider === 'relay') {
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
- for (;;) {
180
- try {
181
- tun = await startTunnel(port, provider, { log, verbose: values.verbose, ...extra });
182
- } catch (err) {
183
- if (interrupted) break;
184
- log(`ERROR: ${err.message}`);
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?.stop?.();
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 (el tunel viejo seguia muriendo).
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
- // Mata el supervisor y tambien el proceso del tunel (aunque el supervisor
270
- // haya muerto y haya quedado huerfano).
271
- if (state?.pid && pidAlive(state.pid)) {
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 : ${shown}`);
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(shown, 2);
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 ${shown}`);
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
- Abre un tunel publico (tunnelmole) hacia el server local para trabajar remoto.
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 tunel en segundo plano + QR (default)
321
- --reload Reinicia el tunel
322
- --stop Cierra el tunel
323
- --status Estado y URL publica
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 tunel sin usuario configurado (peligroso)
252
+ --insecure Permitir el acceso sin usuario configurado (peligroso)
331
253
 
332
- Relay propio (app.json -> tunnel: { provider: "relay", relayUrl, token }): URLs
333
- estables via https://harness.tamnora.com; el QR abre la consola agregada con el
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: 'tunnelmole', domain: '' },
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 { parseTunnelmoleUrls, startTunnelmole, startRelay, startTunnel, tunnelState, tunnelActive } from './tunnel.js';
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,