@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 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
- La via recomendada es **Tailscale** (VPN mesh privada, rapida y sin exponer nada
148
- a Internet). El server sigue escuchando en `127.0.0.1`; `tailscale serve`
149
- publica HTTPS dentro del tailnet y reenvia por loopback, asi que la consola y
150
- todas las tools funcionan igual que en local, sin sincronizar datos (una sola
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 # proveedor default: tailscale (URL + QR)
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
- Proveedores **legacy** (tuneles publicos; utiles solo si en el dispositivo remoto
169
- no podes instalar Tailscale). **Se retiran en la proxima version**; `tunnelmole`
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 (legacy: tunnelmole / relay, se retiran en la proxima
24
- version)**: la URL es accesible desde Internet. Configura un **usuario** con
25
- **contrasena fuerte**; nunca expongas un server sin login. El tunnel reenvia
26
- por loopback, de modo que una vez autenticado se alcanza toda la consola:
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] 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,
@@ -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: String(values.tunnel || app.tunnel?.provider || 'tailscale').toLowerCase(),
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 tunel esta desactivado (provider "none"). Usa --tunnel tailscale (recomendado) o --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.');
102
77
  return 1;
103
78
  }
104
79
 
105
- // Exponer el server a Internet sin usuario configurado deja /api/* abierto
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: 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.');
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
- if (provider === 'tailscale' || provider === 'ts') {
116
- try {
117
- const status = await probeTailscale();
118
- if (!status.connected) throw new Error('Tailscale no esta conectado. Inicia sesion con `tailscale up`.');
119
- } catch (error) {
120
- console.error(error.message);
121
- console.error('Instala Tailscale (https://tailscale.com/download) o usa --tunnel tunnelmole (legacy).');
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), '--tunnel', provider];
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 (${provider}) en el puerto ${port}...`);
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 runTailscaleForeground(values, port) {
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 (el tunel viejo seguia muriendo).
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?.provider === 'tailscale') {
340
- try {
341
- await resetTailscaleServe();
342
- stopped.push('tailscale serve');
343
- } catch (error) {
344
- console.log(`No pude ejecutar tailscale serve reset: ${error.message}`);
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 y tambien el proceso del tunel (aunque el supervisor
348
- // haya muerto y haya quedado huerfano).
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 : ${shown}`);
223
+ console.log(` url : ${state.url}`);
380
224
  console.log(` proveedor: ${state.provider} puerto local: ${state.port}`);
381
- if (state.provider === 'tailscale') console.log(' red : privada (tailnet; solo tus dispositivos)');
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(shown, 2);
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 ${shown}`);
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
- --tunnel <prov> Proveedor (default: el de app.json; hoy: tailscale)
407
- tailscale VPN privada via \`tailscale serve\` (recomendado)
408
- tunnelmole tunel publico gratis, URL aleatoria (legacy*)
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
- * Los tuneles publicos (tunnelmole/relay) se retiran en la proxima version.
414
- tunnelmole se conserva solo para el feature de "preview".`);
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 quedo persistido en instalaciones previas:
33
- // ahora es legacy, asi que se migra a tailscale (seguis pudiendo forzar
34
- // `--tunnel tunnelmole` en cada corrida).
35
- if (app.tunnel.provider === 'tunnelmole' || app.tunnel.provider === 'tmole') {
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
 
@@ -101,7 +101,6 @@ export {
101
101
  resetTailscaleServe,
102
102
  startTailscaleServe,
103
103
  startTunnelmole,
104
- startRelay,
105
104
  startTunnel,
106
105
  tunnelState,
107
106
  tunnelActive,