@danieltmn/tamnora 0.23.0 → 0.23.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 +6 -0
- package/harness/packages/cli/bin/tamnora.mjs +4 -2
- package/harness/packages/cli/src/commands/bridge.js +3 -3
- package/harness/packages/cli/src/commands/update.js +13 -2
- package/harness/packages/cli/test/bin-warning.test.js +27 -0
- package/harness/packages/cli/test/tunnel.test.js +49 -1
- package/harness/packages/core/src/index.js +1 -1
- package/harness/packages/core/src/tunnel.js +32 -9
- package/harness/packages/server/bin/tamnora-server.mjs +4 -2
- package/harness/packages/server/public/index.html +1 -1
- package/harness/packages/server/src/lib/procs.js +4 -1
- package/harness/packages/server/test/procs-manager.test.js +17 -0
- package/harness/scripts/self-update.mjs +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.23.1] - 2026-10-28
|
|
9
|
+
|
|
10
|
+
### Cambios
|
|
11
|
+
|
|
12
|
+
- fix(spawn): evita el warning DEP0190 y hace puntual el stop del bridge
|
|
13
|
+
|
|
8
14
|
## [0.23.0] - 2026-10-28
|
|
9
15
|
|
|
10
16
|
### Cambios
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// node:sqlite sigue siendo "experimental" en Node 22, que avisa aunque ande. El
|
|
3
|
-
// aviso no le aporta nada al usuario, asi que se filtra aca
|
|
4
|
-
//
|
|
3
|
+
// aviso no le aporta nada al usuario, asi que se filtra aca. Se quita el
|
|
4
|
+
// impresor default de Node para que lo que si se imprime (lo que no sea
|
|
5
|
+
// ExperimentalWarning) salga una sola vez, no dos.
|
|
6
|
+
process.removeAllListeners('warning');
|
|
5
7
|
process.on('warning', (warning) => {
|
|
6
8
|
if (warning.name !== 'ExperimentalWarning') console.warn(warning.stack ?? warning);
|
|
7
9
|
});
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
tailLines,
|
|
12
12
|
bridgeLogPath,
|
|
13
13
|
} from '../lib/daemon.js';
|
|
14
|
-
import { startTunnel, probeTailscale,
|
|
14
|
+
import { startTunnel, probeTailscale, stopTailscaleServe } from '../lib/tunnel.js';
|
|
15
15
|
import { qrTerminal } from '../lib/qr.js';
|
|
16
16
|
|
|
17
17
|
const OPTIONS = {
|
|
@@ -193,10 +193,10 @@ async function cmdStop() {
|
|
|
193
193
|
return 0;
|
|
194
194
|
}
|
|
195
195
|
try {
|
|
196
|
-
await
|
|
196
|
+
await stopTailscaleServe();
|
|
197
197
|
stopped.push('tailscale serve');
|
|
198
198
|
} catch (error) {
|
|
199
|
-
console.log(`No pude
|
|
199
|
+
console.log(`No pude detener tailscale serve: ${error.message}`);
|
|
200
200
|
}
|
|
201
201
|
// Mata el supervisor si quedo vivo.
|
|
202
202
|
if (state.pid && pidAlive(state.pid)) {
|
|
@@ -16,8 +16,15 @@ const OPTIONS = {
|
|
|
16
16
|
help: { type: 'boolean', short: 'h' },
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// Con `shell: true` (npm.cmd en Windows) Node concatena los args sin escapar
|
|
20
|
+
// (warning DEP0190): se manda un unico comando con args internos ya validados.
|
|
21
|
+
function npmSpawn(args, extra = {}) {
|
|
22
|
+
const [cmd, cmdArgs] = IS_WIN ? [[NPM, ...args].join(' '), []] : [NPM, args];
|
|
23
|
+
return spawnSync(cmd, cmdArgs, { windowsHide: true, shell: IS_WIN, timeout: 180000, ...extra });
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
function npm(args) {
|
|
20
|
-
return
|
|
27
|
+
return npmSpawn(args, { encoding: 'utf8' });
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/** Version del paquete instalado globalmente (la que resuelve `npm ls -g`). */
|
|
@@ -78,6 +85,10 @@ export async function updateCommand(argv) {
|
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
const tag = typeof values.tag === 'string' && values.tag ? values.tag : 'latest';
|
|
88
|
+
if (!/^[\w.-]+$/.test(tag)) {
|
|
89
|
+
console.error(`Tag invalido: ${tag}`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
81
92
|
const spec = `${PACKAGE}@${tag}`;
|
|
82
93
|
const version = readPackageVersion();
|
|
83
94
|
console.log(`Version actual: ${version}`);
|
|
@@ -115,7 +126,7 @@ export async function updateCommand(argv) {
|
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
console.log(`Actualizando a ${spec}...`);
|
|
118
|
-
const result =
|
|
129
|
+
const result = npmSpawn(['i', '-g', spec], { stdio: 'inherit' });
|
|
119
130
|
if (result.status !== 0) {
|
|
120
131
|
console.error(`La actualizacion fallo. Proba a mano: npm i -g ${spec}`);
|
|
121
132
|
if (wasRunning) restartServer(wasRunning);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const BIN = fileURLToPath(new URL('../bin/tamnora.mjs', import.meta.url));
|
|
10
|
+
|
|
11
|
+
// El bin filtra el ExperimentalWarning de node:sqlite, pero no debe duplicar el
|
|
12
|
+
// resto: sin quitar el impresor default de Node, cada warning salia dos veces.
|
|
13
|
+
// El probe difiere el warning para que salte con el handler del bin ya instalado.
|
|
14
|
+
test('el bin imprime cada warning una sola vez', async () => {
|
|
15
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'tamnora-bin-'));
|
|
16
|
+
const probe = path.join(dir, 'probe.cjs');
|
|
17
|
+
await writeFile(probe, `setTimeout(() => process.emitWarning('aviso de prueba', 'CustomWarning'), 100);`);
|
|
18
|
+
const res = spawnSync(process.execPath, ['--require', probe, BIN, '--help'], {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
});
|
|
23
|
+
const out = `${res.stdout}\n${res.stderr}`;
|
|
24
|
+
const count = out.split('aviso de prueba').length - 1;
|
|
25
|
+
assert.equal(count, 1, `se esperaba 1 impresion del warning, hubo ${count}`);
|
|
26
|
+
assert.equal(res.status, 0);
|
|
27
|
+
});
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import {
|
|
5
|
+
parseTunnelmoleUrls,
|
|
6
|
+
parseTailscaleStatus,
|
|
7
|
+
startTunnel,
|
|
8
|
+
startTunnelmole,
|
|
9
|
+
stopTailscaleServe,
|
|
10
|
+
tailscalePreviewUrl,
|
|
11
|
+
} from '../src/lib/tunnel.js';
|
|
4
12
|
import { qrTerminal } from '../src/lib/qr.js';
|
|
5
13
|
|
|
14
|
+
function fakeChild(exitCode = 0) {
|
|
15
|
+
const child = new EventEmitter();
|
|
16
|
+
child.stdout = new EventEmitter();
|
|
17
|
+
child.stderr = new EventEmitter();
|
|
18
|
+
child.pid = 0;
|
|
19
|
+
child.killed = false;
|
|
20
|
+
setImmediate(() => child.emit('exit', exitCode));
|
|
21
|
+
return child;
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
test('parseTunnelmoleUrls extracts the https URL', () => {
|
|
7
25
|
const out = parseTunnelmoleUrls('Listening at https://abc-123.tunnelmole.net (forwarding to 4517)');
|
|
8
26
|
assert.equal(out.https, 'https://abc-123.tunnelmole.net');
|
|
@@ -38,6 +56,36 @@ test('startTunnel con provider none no abre nada', async () => {
|
|
|
38
56
|
assert.equal(tun.url, '');
|
|
39
57
|
});
|
|
40
58
|
|
|
59
|
+
test('stopTailscaleServe apaga solo el handler 443 y lanza tailscale sin shell', async () => {
|
|
60
|
+
const calls = [];
|
|
61
|
+
const spawnImpl = (cmd, args, options) => {
|
|
62
|
+
calls.push({ cmd, args, options });
|
|
63
|
+
return fakeChild();
|
|
64
|
+
};
|
|
65
|
+
await stopTailscaleServe({ spawnImpl });
|
|
66
|
+
assert.equal(calls.length, 1);
|
|
67
|
+
assert.equal(calls[0].cmd, 'tailscale');
|
|
68
|
+
assert.deepEqual(calls[0].args, ['serve', '--https=443', 'off']);
|
|
69
|
+
assert.equal(calls[0].options.shell, undefined);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test(
|
|
73
|
+
'startTunnelmole con shell manda un unico comando (Node no concatena args)',
|
|
74
|
+
{ skip: process.platform !== 'win32' },
|
|
75
|
+
async () => {
|
|
76
|
+
const calls = [];
|
|
77
|
+
const spawnImpl = (cmd, args, options) => {
|
|
78
|
+
calls.push({ cmd, args, options });
|
|
79
|
+
return fakeChild(1);
|
|
80
|
+
};
|
|
81
|
+
await assert.rejects(() => startTunnelmole(4517, { spawnImpl }), /termino/);
|
|
82
|
+
assert.equal(calls.length, 1);
|
|
83
|
+
assert.equal(calls[0].cmd, 'npx --yes tunnelmole 4517');
|
|
84
|
+
assert.deepEqual(calls[0].args, []);
|
|
85
|
+
assert.equal(calls[0].options.shell, true);
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
41
89
|
test('tailscalePreviewUrl usa el 8443 y omite el 443', () => {
|
|
42
90
|
assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net'), 'https://pc.tailnet.ts.net:8443');
|
|
43
91
|
assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net', 9000), 'https://pc.tailnet.ts.net:9000');
|
|
@@ -71,14 +71,19 @@ function spawnTunnel(bin, args, opts = {}) {
|
|
|
71
71
|
const parse = opts.parse || (() => ({ https: '', http: '' }));
|
|
72
72
|
const timeoutMs = opts.timeoutMs || 90000;
|
|
73
73
|
const label = opts.label || bin;
|
|
74
|
+
const useShell = process.platform === 'win32';
|
|
74
75
|
return new Promise((resolve, reject) => {
|
|
75
76
|
let child;
|
|
76
77
|
try {
|
|
77
|
-
|
|
78
|
+
// En Windows `npx` es un shim .cmd y exige shell; ahi se manda un unico
|
|
79
|
+
// comando (ver shellCommandLine): con array de args Node concatena sin
|
|
80
|
+
// escapar y emite el warning DEP0190.
|
|
81
|
+
const [cmd, cmdArgs] = useShell ? [shellCommandLine(bin, args), []] : [bin, args];
|
|
82
|
+
child = (opts.spawnImpl || spawn)(cmd, cmdArgs, {
|
|
78
83
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
79
84
|
windowsHide: true,
|
|
80
|
-
shell:
|
|
81
|
-
detached:
|
|
85
|
+
shell: useShell,
|
|
86
|
+
detached: !useShell,
|
|
82
87
|
});
|
|
83
88
|
} catch (err) {
|
|
84
89
|
reject(new Error(`no se pudo iniciar ${label}: ${err.message}`));
|
|
@@ -138,15 +143,29 @@ export function startTunnelmole(port, opts = {}) {
|
|
|
138
143
|
* tailnet reenviando a loopback, asi que no hay exposicion publica y las
|
|
139
144
|
* peticiones siguen siendo "locales" para el server. Proveedor recomendado.
|
|
140
145
|
*/
|
|
146
|
+
// Con `shell: true` Node concatena los args sin escapar (warning DEP0190): se
|
|
147
|
+
// arma un unico comando con args validados contra metacaracteres del shell.
|
|
148
|
+
const SAFE_SHELL_ARG = /^[^&|<>^%!"\r\n]+$/;
|
|
149
|
+
|
|
150
|
+
function shellCommandLine(bin, args) {
|
|
151
|
+
for (const arg of args) {
|
|
152
|
+
if (!SAFE_SHELL_ARG.test(String(arg))) {
|
|
153
|
+
throw new Error(`argumento no permitido para ${bin}: "${arg}"`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [bin, ...args].join(' ');
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
function runTailscale(args, opts = {}) {
|
|
142
160
|
const timeoutMs = opts.timeoutMs || 15000;
|
|
143
161
|
return new Promise((resolve, reject) => {
|
|
144
162
|
let child;
|
|
145
163
|
try {
|
|
146
|
-
|
|
164
|
+
// `tailscale` es un ejecutable real (.exe en Windows): resuelve por PATH
|
|
165
|
+
// sin shell, que ademas evita el warning DEP0190 de Node.
|
|
166
|
+
child = (opts.spawnImpl || spawn)('tailscale', args, {
|
|
147
167
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
148
168
|
windowsHide: true,
|
|
149
|
-
shell: process.platform === 'win32',
|
|
150
169
|
});
|
|
151
170
|
} catch (err) {
|
|
152
171
|
reject(err);
|
|
@@ -241,7 +260,7 @@ export async function startTailscaleServe(port, opts = {}) {
|
|
|
241
260
|
pid: 0,
|
|
242
261
|
child: null,
|
|
243
262
|
probeUrl: `${url}/api/health`,
|
|
244
|
-
stop: () =>
|
|
263
|
+
stop: () => stopTailscaleServe().catch(() => {}),
|
|
245
264
|
};
|
|
246
265
|
}
|
|
247
266
|
|
|
@@ -279,9 +298,13 @@ export async function probeTailscale() {
|
|
|
279
298
|
return parseTailscaleStatus(stdout);
|
|
280
299
|
}
|
|
281
300
|
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Apaga el handler `serve` de la consola (https 443) de forma puntual: un
|
|
303
|
+
* `serve reset` borraria tambien los otros handlers (p.ej. el preview en 8443).
|
|
304
|
+
*/
|
|
305
|
+
export function stopTailscaleServe(opts = {}) {
|
|
306
|
+
const httpsPort = Number(opts.httpsPort) || 443;
|
|
307
|
+
return runTailscale(['serve', `--https=${httpsPort}`, 'off'], { timeoutMs: 30000, spawnImpl: opts.spawnImpl });
|
|
285
308
|
}
|
|
286
309
|
|
|
287
310
|
/**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// node:sqlite sigue siendo "experimental" en Node 22, que avisa aunque ande. El
|
|
3
|
-
// aviso no le aporta nada al usuario, asi que se filtra aca
|
|
4
|
-
//
|
|
3
|
+
// aviso no le aporta nada al usuario, asi que se filtra aca. Se quita el
|
|
4
|
+
// impresor default de Node para que lo que si se imprime (lo que no sea
|
|
5
|
+
// ExperimentalWarning) salga una sola vez, no dos.
|
|
6
|
+
process.removeAllListeners('warning');
|
|
5
7
|
process.on('warning', (warning) => {
|
|
6
8
|
if (warning.name !== 'ExperimentalWarning') console.warn(warning.stack ?? warning);
|
|
7
9
|
});
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
</head>
|
|
76
76
|
<body>
|
|
77
77
|
<header class="topbar">
|
|
78
|
-
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.23.
|
|
78
|
+
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.23.1</span></div>
|
|
79
79
|
<div class="settings">
|
|
80
80
|
<button id="new-session-top" type="button" class="btn topbar-new" title="nueva sesión" aria-label="nueva sesión">
|
|
81
81
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
|
|
@@ -76,7 +76,10 @@ export function createProcManager({ allow } = {}) {
|
|
|
76
76
|
throw new Error(`Argumento no permitido con shell en Windows: "${bad}".`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// Con `shell: true` y array de args Node concatena sin escapar (warning
|
|
80
|
+
// DEP0190): se manda un unico comando (los args ya pasaron SAFE_SHELL_ARG).
|
|
81
|
+
const [cmd, cmdArgs] = shell ? [`${bin} ${list.join(' ')}`, []] : [bin, list];
|
|
82
|
+
const child = spawn(cmd, cmdArgs, {
|
|
80
83
|
cwd,
|
|
81
84
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
85
|
windowsHide: true,
|
|
@@ -29,6 +29,23 @@ test('en Windows los args no admiten metacaracteres del shell', { skip: process.
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
test('un comando bare con args no dispara el warning DEP0190 de Node', { skip: process.platform !== 'win32' }, async () => {
|
|
33
|
+
const cwd = await workspace();
|
|
34
|
+
const procs = createProcManager({ allow: ['node'] });
|
|
35
|
+
const warnings = [];
|
|
36
|
+
const onWarn = (warning) => warnings.push(warning);
|
|
37
|
+
process.on('warning', onWarn);
|
|
38
|
+
let proc;
|
|
39
|
+
try {
|
|
40
|
+
proc = procs.start({ cwd, command: 'node', args: ['-e', 'process.exit(0)'] });
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
42
|
+
} finally {
|
|
43
|
+
process.removeListener('warning', onWarn);
|
|
44
|
+
if (proc) procs.stop(proc.id);
|
|
45
|
+
}
|
|
46
|
+
assert.deepEqual(warnings.filter((w) => w.code === 'DEP0190'), []);
|
|
47
|
+
});
|
|
48
|
+
|
|
32
49
|
test('procs arranca y detiene un proceso permitido', async () => {
|
|
33
50
|
const cwd = await workspace();
|
|
34
51
|
const procs = createProcManager({ allow: ['node'] });
|
|
@@ -77,8 +77,11 @@ async function waitDead(p, ms = 15000) {
|
|
|
77
77
|
return true;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// Con `shell: true` (npm.cmd en Windows) Node concatena los args sin escapar
|
|
81
|
+
// (warning DEP0190): se manda un unico comando con args internos.
|
|
80
82
|
function runNpm(args) {
|
|
81
|
-
|
|
83
|
+
const [cmd, cmdArgs] = IS_WIN ? [[NPM, ...args].join(' '), []] : [NPM, args];
|
|
84
|
+
return spawnSync(cmd, cmdArgs, { encoding: 'utf8', windowsHide: true, shell: IS_WIN, timeout: 180000 });
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
async function main() {
|
package/package.json
CHANGED