nio-cat 0.3.1 → 0.3.2
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/bin/nio.js +15 -5
- package/package.json +1 -1
- package/src/pair.js +20 -9
- package/src/server.js +3 -2
- package/web/app/app.js +5 -3
package/bin/nio.js
CHANGED
|
@@ -11,7 +11,7 @@ const { Hub } = require('../src/hub');
|
|
|
11
11
|
const { createServer } = require('../src/server');
|
|
12
12
|
const { detectAll } = require('../src/agents');
|
|
13
13
|
const { startTunnel } = require('../src/tunnel');
|
|
14
|
-
const { pairLink, terminalQr, writePairPage, makeCode } = require('../src/pair');
|
|
14
|
+
const { pairLink, terminalQr, writePairPage, makeCode, codeLink, terminalQrSize } = require('../src/pair');
|
|
15
15
|
const { which } = require('../src/proc');
|
|
16
16
|
|
|
17
17
|
const argv = process.argv.slice(2);
|
|
@@ -82,10 +82,20 @@ function openDashboard() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async function showPair(publicUrl) {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const app = flags.app || cfg.app;
|
|
86
|
+
const link = pairLink(app, publicUrl, cfg.token), code = makeCode(publicUrl, cfg.token), qrLink = codeLink(app, code);
|
|
87
|
+
const file = writePairPage(link, code, qrLink);
|
|
88
|
+
// El QR ha de cabre al terminal: marge normal, marge d'1 mòdul i, si no hi cap, la pàgina del navegador.
|
|
89
|
+
const rows = process.stdout.rows || 999, cols = process.stdout.columns || 999;
|
|
90
|
+
const fits = (m) => { const z = terminalQrSize(qrLink, m); return z.rows + 9 <= rows && z.cols + 2 <= cols; };
|
|
91
|
+
const margin = fits(2) ? 2 : fits(1) ? 1 : 0;
|
|
92
|
+
if (margin) {
|
|
93
|
+
console.log(`\n ${c.b('Escaneja aquest codi amb el mòbil')}\n`);
|
|
94
|
+
console.log(terminalQr(qrLink, { margin }));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`\n ${c.warn('El terminal és massa petit per al QR.')} Fes-lo més gran o obre ${file}`);
|
|
97
|
+
if (canOpenBrowser()) openUrl(file);
|
|
98
|
+
}
|
|
89
99
|
console.log(`\n ${c.b('Codi:')} ${code}`);
|
|
90
100
|
console.log(c.dim(' Copia\'l i enganxa\'l a Sessions → Tria un ordinador.'));
|
|
91
101
|
console.log(c.dim(' Aquest codi dona accés als teus agents. Guarda\'l com una contrasenya.\n'));
|
package/package.json
CHANGED
package/src/pair.js
CHANGED
|
@@ -16,13 +16,17 @@ function pairLink(app, url, token) {
|
|
|
16
16
|
// La web ja sap posar-hi l'enllaç de davant. Les adreces de Cloudflare es comprimeixen (≈ 60 caràcters).
|
|
17
17
|
const CF = '.trycloudflare.com';
|
|
18
18
|
function makeCode(url, token) {
|
|
19
|
-
const raw = Buffer.from(token, 'base64url');
|
|
19
|
+
const raw = Buffer.from(token, 'base64url'), tok24 = raw.length === 24 && raw.toString('base64url') === token;
|
|
20
20
|
const sub = url.startsWith('https://') && url.endsWith(CF) ? url.slice(8, -CF.length) : null;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const lan = url.match(/^http:\/\/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}):(\d{1,5})$/);
|
|
22
|
+
let bytes;
|
|
23
|
+
if (tok24 && sub && /^[\w-]+$/.test(sub)) bytes = Buffer.concat([Buffer.from([1]), raw, Buffer.from(sub)]);
|
|
24
|
+
else if (tok24 && lan && lan.slice(1, 5).every((n) => +n < 256) && +lan[5] < 65536) bytes = Buffer.concat([Buffer.from([2]), raw, Buffer.from(lan.slice(1, 5).map(Number)), Buffer.from([+lan[5] >> 8, +lan[5] & 255])]);
|
|
25
|
+
else bytes = Buffer.concat([Buffer.from([0]), Buffer.from(`${url}\n${token}`)]);
|
|
24
26
|
return `nio-${bytes.toString('base64url')}`;
|
|
25
27
|
}
|
|
28
|
+
// Enllaç curt per al QR: una adreça de l'app amb el codi al fragment. Un QR més petit cap millor al terminal.
|
|
29
|
+
const codeLink = (app, code) => `${app.replace(/\/$/, '')}/app#${code}`;
|
|
26
30
|
|
|
27
31
|
function qr(text) {
|
|
28
32
|
const q = qrcode(0, 'L');
|
|
@@ -33,10 +37,10 @@ function qr(text) {
|
|
|
33
37
|
|
|
34
38
|
// QR per al terminal: cada mòdul són dos espais amb color de fons (negre o blanc). No depèn de cap glif,
|
|
35
39
|
// així que no es talla amb tipus de lletra o interlineats estranys. Sempre negre sobre blanc, amb tema clar o fosc.
|
|
36
|
-
function terminalQr(text, { color = !process.env.NO_COLOR } = {}) {
|
|
40
|
+
function terminalQr(text, { color = !process.env.NO_COLOR, margin = 2 } = {}) {
|
|
37
41
|
const q = qr(text);
|
|
38
42
|
const n = q.getModuleCount();
|
|
39
|
-
const M =
|
|
43
|
+
const M = margin; // marge blanc al voltant, en mòduls
|
|
40
44
|
const dark = (r, c) => r >= 0 && c >= 0 && r < n && c < n && q.isDark(r, c);
|
|
41
45
|
const lines = [];
|
|
42
46
|
for (let r = -M; r < n + M; r++) {
|
|
@@ -72,8 +76,8 @@ function qrSvg(text) {
|
|
|
72
76
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" role="img" aria-label="Codi QR"><rect width="${size}" height="${size}" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
function writePairPage(link, code) {
|
|
76
|
-
const svg = qrSvg(link);
|
|
79
|
+
function writePairPage(link, code, qrLink) {
|
|
80
|
+
const svg = qrSvg(qrLink || link);
|
|
77
81
|
const html = `<!doctype html><html lang="ca"><head><meta charset="utf-8">
|
|
78
82
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>nio // connecta el mòbil</title>
|
|
79
83
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -88,4 +92,11 @@ code{display:block;word-break:break-all;font:12px/1.5 ui-monospace,Menlo,monospa
|
|
|
88
92
|
return file;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode };
|
|
95
|
+
module.exports = { pairLink, terminalQr, writePairPage, qrSvg, makeCode, codeLink };
|
|
96
|
+
|
|
97
|
+
// Mida del QR al terminal: files i columnes que ocupa amb un marge donat.
|
|
98
|
+
function terminalQrSize(text, margin = 2) {
|
|
99
|
+
const n = qr(text).getModuleCount() + margin * 2;
|
|
100
|
+
return { rows: n, cols: n * 2 + 2 };
|
|
101
|
+
}
|
|
102
|
+
module.exports.terminalQrSize = terminalQrSize;
|
package/src/server.js
CHANGED
|
@@ -10,7 +10,7 @@ const ollama = require('./agents/ollama');
|
|
|
10
10
|
const { save } = require('./config');
|
|
11
11
|
const { httpErr } = require('./hub');
|
|
12
12
|
const github = require('./github');
|
|
13
|
-
const { pairLink, qrSvg, makeCode } = require('./pair');
|
|
13
|
+
const { pairLink, qrSvg, makeCode, codeLink } = require('./pair');
|
|
14
14
|
|
|
15
15
|
// L'app web: copiada dins del paquet en publicar-lo, o l'arrel del repositori en desenvolupament.
|
|
16
16
|
const WEB_DIRS = [path.join(__dirname, '..', 'web'), path.join(__dirname, '..', '..')];
|
|
@@ -232,7 +232,8 @@ function createServer({ cfg, hub, getPublicUrl, getApp }) {
|
|
|
232
232
|
const pub = getPublicUrl();
|
|
233
233
|
if (!pub || /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(pub)) return { link: null };
|
|
234
234
|
const link = pairLink(getApp ? getApp() : cfg.app, pub, cfg.token);
|
|
235
|
-
|
|
235
|
+
const code = makeCode(pub, cfg.token);
|
|
236
|
+
return { link, code, svg: qrSvg(codeLink(getApp ? getApp() : cfg.app, code)), url: pub, lan: /^http:\/\//.test(pub) };
|
|
236
237
|
}
|
|
237
238
|
if (p === '/api/github' && method === 'GET') return github.status();
|
|
238
239
|
if (p === '/api/github/repos' && method === 'GET') return github.repos(url.searchParams.get('owner') || '');
|
package/web/app/app.js
CHANGED
|
@@ -194,10 +194,12 @@ function parsePair(text) {
|
|
|
194
194
|
// Codi «nio-…»: porta l'adreça i la clau dins (vegeu makeCode al programa). Es descodifica aquí, sense servidor.
|
|
195
195
|
const CF = '.trycloudflare.com';
|
|
196
196
|
function parseCode(text) {
|
|
197
|
-
const m = String(text || '').replace(/\s+/g, '').match(
|
|
197
|
+
const m = String(text || '').replace(/\s+/g, '').match(/(?:^|#)nio-([\w-]+)$/i); if (!m) return null;
|
|
198
198
|
try {
|
|
199
199
|
const bin = atob(m[1].replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (m[1].length % 4)) % 4)), u = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
200
|
-
|
|
200
|
+
const tok = () => btoa(String.fromCharCode(...u.slice(1, 25))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
201
|
+
if (u[0] === 1) return { url: `https://${new TextDecoder().decode(u.slice(25))}${CF}`, token: tok() };
|
|
202
|
+
if (u[0] === 2) return { url: `http://${[...u.slice(25, 29)].join('.')}:${(u[29] << 8) | u[30]}`, token: tok() };
|
|
201
203
|
if (u[0] === 0) { const [url, token] = new TextDecoder().decode(u.slice(1)).split('\n'); if (url && token) return { url: url.replace(/\/$/, ''), token }; }
|
|
202
204
|
} catch { /* codi malmès */ }
|
|
203
205
|
return null;
|
|
@@ -212,7 +214,7 @@ function addMachine({ url, token, name }) {
|
|
|
212
214
|
connect();
|
|
213
215
|
}
|
|
214
216
|
function checkPairHash() {
|
|
215
|
-
const pair = parsePair(location.hash); if (!pair) return false;
|
|
217
|
+
const pair = parsePair(location.hash) || parseCode(location.hash); if (!pair) return false;
|
|
216
218
|
history.replaceState(null, '', location.pathname + '#/sessions');
|
|
217
219
|
state.route = { name: 'sessions' };
|
|
218
220
|
addMachine(pair); render(); toast('Ordinador connectat'); return true;
|