@danieltmn/openbridge 0.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/bin/openbridge.js +18 -0
  5. package/docs/ARCHITECTURE.md +77 -0
  6. package/package.json +54 -0
  7. package/src/auth.js +204 -0
  8. package/src/bridge/bridge.js +2280 -0
  9. package/src/cli.js +791 -0
  10. package/src/config.js +108 -0
  11. package/src/log.js +22 -0
  12. package/src/paths.js +152 -0
  13. package/src/push.js +85 -0
  14. package/src/store/index.js +930 -0
  15. package/src/store/jsonfile.js +54 -0
  16. package/src/tunnel/index.js +141 -0
  17. package/src/web/assets/app.js +3939 -0
  18. package/src/web/assets/icons/icon-128x128.png +0 -0
  19. package/src/web/assets/icons/icon-144x144.png +0 -0
  20. package/src/web/assets/icons/icon-152x152.png +0 -0
  21. package/src/web/assets/icons/icon-192x192.png +0 -0
  22. package/src/web/assets/icons/icon-384x384.png +0 -0
  23. package/src/web/assets/icons/icon-512x512.png +0 -0
  24. package/src/web/assets/icons/icon-72x72.png +0 -0
  25. package/src/web/assets/icons/icon-96x96.png +0 -0
  26. package/src/web/assets/manifest.webmanifest +24 -0
  27. package/src/web/assets/sw.js +85 -0
  28. package/src/web/assets/themes/dark-plus.css +19 -0
  29. package/src/web/assets/themes/dark-red.css +18 -0
  30. package/src/web/assets/themes/default.css +18 -0
  31. package/src/web/assets/themes/github-light.css +19 -0
  32. package/src/web/assets/themes/high-contrast.css +18 -0
  33. package/src/web/assets/themes/index.json +15 -0
  34. package/src/web/assets/themes/light-plus.css +19 -0
  35. package/src/web/assets/themes/light.css +18 -0
  36. package/src/web/assets/themes/monokai.css +19 -0
  37. package/src/web/assets/themes/one-dark.css +19 -0
  38. package/src/web/assets/themes/solarized.css +18 -0
  39. package/src/web/assets/themes/terminal.css +28 -0
  40. package/src/web/assets/themes/themes.css +2 -0
  41. package/src/web/routes.js +871 -0
  42. package/src/web/server.js +130 -0
  43. package/src/web/templates/chat.html +1296 -0
  44. package/src/web/templates/login.html +144 -0
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Lectura/escritura de JSON con serializacion por archivo.
5
+ *
6
+ * El server Node es el unico escritor de `data/` (el puente habla por HTTP, no
7
+ * toca los archivos), asi que un mutex en memoria alcanza. La escritura va a un
8
+ * .tmp y rename() para que los lectores nunca vean un archivo a medio escribir.
9
+ */
10
+
11
+ const fs = require('node:fs/promises');
12
+
13
+ const locks = new Map();
14
+
15
+ function withLock(key, fn) {
16
+ const prev = locks.get(key) || Promise.resolve();
17
+ const run = prev.then(fn, fn);
18
+ // La cadena no debe romperse por un error de un eslabon.
19
+ locks.set(key, run.then(() => {}, () => {}));
20
+ return run;
21
+ }
22
+
23
+ async function readJson(file, fallback) {
24
+ try {
25
+ const raw = await fs.readFile(file, 'utf8');
26
+ const data = JSON.parse(raw);
27
+ return (data && typeof data === 'object') ? data : structuredClone(fallback);
28
+ } catch (e) {
29
+ return structuredClone(fallback);
30
+ }
31
+ }
32
+
33
+ async function writeAtomic(file, data) {
34
+ const tmp = file + '.' + process.pid + '.' + Math.random().toString(36).slice(2) + '.tmp';
35
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2));
36
+ await fs.rename(tmp, file);
37
+ }
38
+
39
+ /**
40
+ * Lee, deja modificar con fn(data) y guarda. Si fn devuelve false, no guarda.
41
+ * Si fn devuelve un objeto, ese objeto reemplaza al leido.
42
+ */
43
+ async function update(file, fallback, fn) {
44
+ return withLock(file, async () => {
45
+ let data = await readJson(file, fallback);
46
+ const res = await fn(data);
47
+ if (res === false) return false;
48
+ if (res && typeof res === 'object') data = res;
49
+ await writeAtomic(file, data);
50
+ return data;
51
+ });
52
+ }
53
+
54
+ module.exports = { withLock, readJson, writeAtomic, update };
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Tuneles publicos. Proveedores soportados:
5
+ * - tunnelmole (gratis, sin cuenta; URL aleatoria) -> `npx tunnelmole <puerto>`
6
+ * - ngrok (requiere cuenta + authtoken; admite dominio fijo) -> `ngrok http`
7
+ * - cloudflare (gratis, quick tunnel; URL aleatoria) -> `cloudflared tunnel --url`
8
+ *
9
+ * La interfaz es generica: startTunnel(port, provider, opts) -> { url, provider, pid, stop }.
10
+ */
11
+
12
+ const { spawn } = require('node:child_process');
13
+
14
+ function firstUrl(urls, scheme) {
15
+ const list = [...new Set(urls)];
16
+ return list.find((u) => u.startsWith(scheme)) || '';
17
+ }
18
+
19
+ function parseTunnelmoleUrls(text) {
20
+ const urls = String(text).match(/https?:\/\/[a-z0-9-]+\.tunnelmole\.net/gi) || [];
21
+ return { https: firstUrl(urls, 'https'), http: firstUrl(urls, 'http:') };
22
+ }
23
+
24
+ function parseNgrokUrls(text) {
25
+ const urls = String(text).match(/https?:\/\/[a-z0-9-]+\.ngrok(?:-free)?\.(?:app|io|dev)/gi) || [];
26
+ return { https: firstUrl(urls, 'https'), http: firstUrl(urls, 'http:') };
27
+ }
28
+
29
+ function parseCloudflaredUrls(text) {
30
+ const urls = String(text).match(/https?:\/\/[a-z0-9-]+\.trycloudflare\.com/gi) || [];
31
+ return { https: firstUrl(urls, 'https'), http: firstUrl(urls, 'http:') };
32
+ }
33
+
34
+ function killTree(child) {
35
+ if (!child || child.killed) return;
36
+ const pid = child.pid;
37
+ try {
38
+ if (process.platform === 'win32' && pid) {
39
+ spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
40
+ } else {
41
+ if (pid) { try { process.kill(-pid, 'SIGTERM'); } catch (e) { /* sin grupo */ } }
42
+ try { child.kill('SIGTERM'); } catch (e) { /* ya murio */ }
43
+ }
44
+ } catch (e) { /* ya murio */ }
45
+ }
46
+
47
+ // Lanza un binario y resuelve cuando el parseador encuentra una URL.
48
+ function spawnTunnel(bin, args, opts = {}) {
49
+ const log = opts.log || (() => {});
50
+ const parse = opts.parse || (() => ({ https: '', http: '' }));
51
+ const timeoutMs = opts.timeoutMs || 90000;
52
+ const label = opts.label || bin;
53
+ return new Promise((resolve, reject) => {
54
+ let child;
55
+ try {
56
+ child = spawn(bin, args, {
57
+ stdio: ['ignore', 'pipe', 'pipe'],
58
+ windowsHide: true,
59
+ shell: process.platform === 'win32',
60
+ detached: process.platform !== 'win32',
61
+ });
62
+ } catch (e) {
63
+ reject(new Error('no se pudo iniciar ' + label + ': ' + e.message));
64
+ return;
65
+ }
66
+ let buf = '';
67
+ let settled = false;
68
+ const timer = setTimeout(() => {
69
+ if (settled) return;
70
+ settled = true;
71
+ killTree(child);
72
+ reject(new Error(label + ' no devolvio URL (' + Math.round(timeoutMs / 1000) + ' s). Instalalo o proba otro proveedor.'));
73
+ }, timeoutMs);
74
+ const feed = (d) => {
75
+ buf += String(d);
76
+ if (opts.verbose) process.stdout.write(String(d));
77
+ const urls = parse(buf);
78
+ if (urls.https || urls.http) {
79
+ if (settled) return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ const url = urls.https || urls.http;
83
+ log(label + ': ' + url + ' -> localhost:' + opts.port);
84
+ resolve({ url, provider: opts.provider, pid: child.pid, stop: () => killTree(child) });
85
+ }
86
+ };
87
+ child.stdout.on('data', feed);
88
+ child.stderr.on('data', feed);
89
+ child.on('error', (e) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ clearTimeout(timer);
93
+ reject(new Error(label + ': ' + e.message));
94
+ });
95
+ child.on('exit', (code) => {
96
+ if (settled) return;
97
+ settled = true;
98
+ clearTimeout(timer);
99
+ reject(new Error(label + ' termino (codigo ' + code + ')' + (buf ? ': ' + buf.slice(0, 200).trim() : '')));
100
+ });
101
+ });
102
+ }
103
+
104
+ function startTunnelmole(port, opts = {}) {
105
+ return spawnTunnel('npx', ['--yes', 'tunnelmole', String(port)], {
106
+ ...opts, port, provider: 'tunnelmole', label: 'tunnelmole', parse: parseTunnelmoleUrls,
107
+ });
108
+ }
109
+
110
+ function startNgrok(port, opts = {}) {
111
+ const args = ['http', String(port), '--log', 'stdout', '--log-format', 'json'];
112
+ if (opts.domain) args.push('--domain=' + opts.domain);
113
+ return spawnTunnel('ngrok', args, {
114
+ ...opts, port, provider: 'ngrok', label: 'ngrok', parse: parseNgrokUrls,
115
+ });
116
+ }
117
+
118
+ function startCloudflared(port, opts = {}) {
119
+ const args = ['tunnel', '--no-autoupdate', '--url', 'http://127.0.0.1:' + port];
120
+ return spawnTunnel('cloudflared', args, {
121
+ ...opts, port, provider: 'cloudflare', label: 'cloudflared', parse: parseCloudflaredUrls,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Arranca el tunel segun el proveedor. Devuelve { url, provider, pid, stop }.
127
+ * provider 'none' no abre nada.
128
+ */
129
+ async function startTunnel(port, provider = 'tunnelmole', opts = {}) {
130
+ const p = String(provider || 'tunnelmole').toLowerCase();
131
+ if (p === 'none' || p === 'off' || p === '') return { url: '', provider: 'none', pid: 0, stop: () => {} };
132
+ if (p === 'tunnelmole' || p === 'tmole') return startTunnelmole(port, opts);
133
+ if (p === 'ngrok') return startNgrok(port, opts);
134
+ if (p === 'cloudflare' || p === 'cloudflared') return startCloudflared(port, opts);
135
+ throw new Error('proveedor de tunel no soportado: ' + provider);
136
+ }
137
+
138
+ module.exports = {
139
+ startTunnel, startTunnelmole, startNgrok, startCloudflared,
140
+ parseTunnelmoleUrls, parseNgrokUrls, parseCloudflaredUrls,
141
+ };