@babylen/legion 0.1.1 → 0.1.3
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/dist/index.js +327 -79
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,11 +23,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_child_process = require("child_process");
|
|
28
|
+
|
|
29
|
+
// package.json
|
|
30
|
+
var version = "0.1.3";
|
|
31
|
+
|
|
26
32
|
// src/index.ts
|
|
27
33
|
var import_socket = require("socket.io-client");
|
|
28
|
-
var
|
|
29
|
-
var import_fs2 = require("fs");
|
|
30
|
-
var import_path4 = require("path");
|
|
34
|
+
var os4 = __toESM(require("os"));
|
|
31
35
|
|
|
32
36
|
// src/core/config.ts
|
|
33
37
|
var import_os = __toESM(require("os"));
|
|
@@ -44,6 +48,14 @@ var ConfigSchema = import_zod.z.object({
|
|
|
44
48
|
var HOME_DIR = import_os.default.homedir();
|
|
45
49
|
var CONFIG_DIR = import_path.default.join(HOME_DIR, ".tanuki");
|
|
46
50
|
var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
|
|
51
|
+
async function ensureConfigDir() {
|
|
52
|
+
try {
|
|
53
|
+
await import_promises.default.access(CONFIG_DIR);
|
|
54
|
+
} catch {
|
|
55
|
+
await import_promises.default.mkdir(CONFIG_DIR, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
return CONFIG_FILE;
|
|
58
|
+
}
|
|
47
59
|
async function loadConfig() {
|
|
48
60
|
try {
|
|
49
61
|
const content = await import_promises.default.readFile(CONFIG_FILE, "utf-8");
|
|
@@ -53,6 +65,14 @@ async function loadConfig() {
|
|
|
53
65
|
return null;
|
|
54
66
|
}
|
|
55
67
|
}
|
|
68
|
+
async function saveConfig(config2) {
|
|
69
|
+
await ensureConfigDir();
|
|
70
|
+
await import_promises.default.writeFile(CONFIG_FILE, JSON.stringify(config2, null, 2), "utf-8");
|
|
71
|
+
try {
|
|
72
|
+
await import_promises.default.chmod(CONFIG_FILE, 384);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
56
76
|
async function getConfig() {
|
|
57
77
|
const fileConfig = await loadConfig();
|
|
58
78
|
const id = fileConfig?.id || process.env.LEGION_TOKEN_ID || void 0;
|
|
@@ -72,6 +92,18 @@ async function getConfig() {
|
|
|
72
92
|
}
|
|
73
93
|
return config2;
|
|
74
94
|
}
|
|
95
|
+
async function updateConfig(updates) {
|
|
96
|
+
const currentConfig = await loadConfig();
|
|
97
|
+
if (!currentConfig) {
|
|
98
|
+
throw new Error("Cannot update config: config file does not exist");
|
|
99
|
+
}
|
|
100
|
+
const updatedConfig = {
|
|
101
|
+
...currentConfig,
|
|
102
|
+
...updates
|
|
103
|
+
};
|
|
104
|
+
ConfigSchema.parse(updatedConfig);
|
|
105
|
+
await saveConfig(updatedConfig);
|
|
106
|
+
}
|
|
75
107
|
|
|
76
108
|
// src/util/log.ts
|
|
77
109
|
var import_path3 = __toESM(require("path"));
|
|
@@ -260,104 +292,320 @@ var Log;
|
|
|
260
292
|
Log2.create = create;
|
|
261
293
|
})(Log || (Log = {}));
|
|
262
294
|
|
|
295
|
+
// src/util/fingerprint.ts
|
|
296
|
+
var os3 = __toESM(require("os"));
|
|
297
|
+
var cachedFingerprint = null;
|
|
298
|
+
async function getSystemFingerprint() {
|
|
299
|
+
if (cachedFingerprint) return cachedFingerprint;
|
|
300
|
+
const hostname3 = os3.hostname();
|
|
301
|
+
const platform3 = os3.platform();
|
|
302
|
+
const arch2 = os3.arch();
|
|
303
|
+
const cpuModel = os3.cpus()[0]?.model || "unknown";
|
|
304
|
+
const interfaces = os3.networkInterfaces();
|
|
305
|
+
let macAddress = "";
|
|
306
|
+
for (const iface of Object.values(interfaces || {})) {
|
|
307
|
+
for (const addr of iface || []) {
|
|
308
|
+
if (!addr.internal && addr.mac && addr.mac !== "00:00:00:00:00:00") {
|
|
309
|
+
macAddress = addr.mac;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (macAddress) break;
|
|
314
|
+
}
|
|
315
|
+
const data2 = JSON.stringify({ hostname: hostname3, platform: platform3, arch: arch2, cpuModel, macAddress });
|
|
316
|
+
const encoder = new TextEncoder();
|
|
317
|
+
const dataBuffer = encoder.encode(data2);
|
|
318
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
319
|
+
cachedFingerprint = Buffer.from(hashBuffer).toString("hex");
|
|
320
|
+
return cachedFingerprint;
|
|
321
|
+
}
|
|
322
|
+
|
|
263
323
|
// src/index.ts
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const packageJson = JSON.parse((0, import_fs2.readFileSync)(packageJsonPath, "utf-8"));
|
|
268
|
-
version = packageJson.version;
|
|
269
|
-
} catch {
|
|
270
|
-
version = "0.0.0";
|
|
324
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
325
|
+
console.log(version);
|
|
326
|
+
process.exit(0);
|
|
271
327
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
function parseArgs() {
|
|
329
|
+
const args = process.argv.slice(2);
|
|
330
|
+
const parsed = {};
|
|
331
|
+
for (let i = 0; i < args.length; i++) {
|
|
332
|
+
if (args[i] === "--token" && args[i + 1]) {
|
|
333
|
+
parsed.token = args[i + 1];
|
|
334
|
+
i++;
|
|
335
|
+
} else if (args[i] === "auth" && args[i + 1] === "--token" && args[i + 2]) {
|
|
336
|
+
parsed.command = "auth";
|
|
337
|
+
parsed.token = args[i + 2];
|
|
338
|
+
i += 2;
|
|
339
|
+
} else if (args[i] === "login") {
|
|
340
|
+
parsed.command = "login";
|
|
341
|
+
} else if (args[i] === "--version" || args[i] === "-v") {
|
|
342
|
+
parsed.version = true;
|
|
343
|
+
}
|
|
276
344
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
async function handleDeviceLogin(log, serverUrl = "https://tanuki.sabw.ru") {
|
|
348
|
+
const codeEndpoint = `${serverUrl}/api/v1/legion/device/code`;
|
|
349
|
+
const tokenEndpoint = `${serverUrl}/api/v1/legion/device/token`;
|
|
350
|
+
const codeResponse = await fetch(codeEndpoint, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "Content-Type": "application/json" }
|
|
353
|
+
});
|
|
354
|
+
if (!codeResponse.ok) {
|
|
355
|
+
throw new Error(`Failed to get device code: ${codeResponse.statusText}`);
|
|
356
|
+
}
|
|
357
|
+
const { code, activation_url } = await codeResponse.json();
|
|
358
|
+
console.log(`\u{1F449} Go to: ${activation_url || "https://tanuki.sabw.ru/activate"}`);
|
|
359
|
+
console.log(`\u{1F511} Enter code: ${code}`);
|
|
360
|
+
const pollInterval = 3e3;
|
|
361
|
+
const maxAttempts = 60;
|
|
362
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
364
|
+
const tokenResponse = await fetch(tokenEndpoint, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: { "Content-Type": "application/json" },
|
|
367
|
+
body: JSON.stringify({ code })
|
|
368
|
+
});
|
|
369
|
+
if (tokenResponse.ok) {
|
|
370
|
+
const { token, token_id } = await tokenResponse.json();
|
|
371
|
+
const existingConfig = await loadConfig();
|
|
372
|
+
await saveConfig({
|
|
373
|
+
token,
|
|
374
|
+
id: token_id,
|
|
375
|
+
serverUrl: existingConfig?.serverUrl || "wss://tanuki.sabw.ru"
|
|
376
|
+
});
|
|
377
|
+
log.info("\u2705 Device activated successfully");
|
|
378
|
+
return;
|
|
379
|
+
} else if (tokenResponse.status === 202) {
|
|
380
|
+
continue;
|
|
381
|
+
} else {
|
|
382
|
+
throw new Error(`Failed to get token: ${tokenResponse.statusText}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
throw new Error("Device activation timed out");
|
|
386
|
+
}
|
|
387
|
+
function checkForUpdates() {
|
|
388
|
+
setTimeout(async () => {
|
|
389
|
+
try {
|
|
390
|
+
const packageName = "@babylen/legion";
|
|
391
|
+
const command = `npm view ${packageName} version`;
|
|
392
|
+
const timeout = 3e3;
|
|
393
|
+
const latestVersion = (0, import_child_process.execSync)(command, {
|
|
394
|
+
encoding: "utf-8",
|
|
395
|
+
timeout,
|
|
396
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
397
|
+
}).trim();
|
|
398
|
+
if (latestVersion !== version) {
|
|
399
|
+
console.warn(`
|
|
400
|
+
\u26A0\uFE0F Update available: ${version} \u2192 ${latestVersion}`);
|
|
401
|
+
console.warn(` Run: npm install -g ${packageName}@latest
|
|
402
|
+
`);
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
}
|
|
406
|
+
}, 100);
|
|
407
|
+
}
|
|
408
|
+
async function enterHibernationMode(log, configFile, onConfigChanged) {
|
|
409
|
+
log.warn("\u26D4 Auth failed. Entering hibernation mode. Waiting for config update...");
|
|
410
|
+
const fs4 = await import("fs");
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
const watchHandle = fs4.watch(configFile, async (eventType) => {
|
|
413
|
+
if (eventType === "change") {
|
|
414
|
+
log.info("\u{1F4DD} Config file changed. Attempting reconnection...");
|
|
415
|
+
try {
|
|
416
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
417
|
+
await onConfigChanged();
|
|
418
|
+
watchHandle.close();
|
|
419
|
+
resolve();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
log.error("\u274C Reconnection failed", { error });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
296
424
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function createSocket(config2) {
|
|
428
|
+
return (0, import_socket.io)(config2.serverUrl, {
|
|
429
|
+
auth: {
|
|
430
|
+
id: config2.id,
|
|
431
|
+
// Token ID for faster lookup (optional)
|
|
432
|
+
token: config2.token,
|
|
433
|
+
// Secret token for authentication
|
|
434
|
+
type: "legion"
|
|
435
|
+
},
|
|
436
|
+
transports: ["websocket"],
|
|
437
|
+
reconnection: true,
|
|
438
|
+
reconnectionDelay: 1e3,
|
|
439
|
+
reconnectionDelayMax: 5e3,
|
|
440
|
+
reconnectionAttempts: Infinity
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function setupSocketHandlers(socket, log, fingerprint, version2, onTokenRotation, onReconnect) {
|
|
444
|
+
socket.on("connect", () => {
|
|
445
|
+
log.info("\u2705 Connected to server", { socketId: socket.id });
|
|
446
|
+
const handshakeData = {
|
|
447
|
+
fingerprint,
|
|
448
|
+
version: version2,
|
|
449
|
+
hostname: os4.hostname(),
|
|
450
|
+
platform: os4.platform(),
|
|
451
|
+
release: os4.release(),
|
|
452
|
+
cwd: process.cwd()
|
|
453
|
+
};
|
|
454
|
+
socket.emit("legion:handshake", handshakeData);
|
|
455
|
+
log.debug("\u{1F4E4} Sent handshake", handshakeData);
|
|
456
|
+
});
|
|
457
|
+
socket.on("connect_error", (err) => {
|
|
458
|
+
log.error("\u274C Connection error", {
|
|
459
|
+
message: err.message,
|
|
460
|
+
type: err.type
|
|
307
461
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
462
|
+
if (err.message.includes("auth") || err.message.includes("token")) {
|
|
463
|
+
log.error("\u{1F510} Authentication failed. Please check your token.");
|
|
464
|
+
log.error("\u{1F4A1} Re-authenticate by updating ~/.tanuki/config.json or LEGION_TOKEN env var");
|
|
465
|
+
enterHibernationMode(log, CONFIG_FILE, onReconnect).catch((error) => {
|
|
466
|
+
log.error("\u274C Hibernation mode failed", { error });
|
|
467
|
+
process.exit(1);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
socket.on("disconnect", (reason) => {
|
|
472
|
+
log.warn("\u26A0\uFE0F Disconnected from server", { reason });
|
|
473
|
+
if (reason === "io server disconnect") {
|
|
474
|
+
log.error("\u{1F6AB} Server disconnected this client. Please check your token.");
|
|
475
|
+
enterHibernationMode(log, CONFIG_FILE, onReconnect).catch((error) => {
|
|
476
|
+
log.error("\u274C Hibernation mode failed", { error });
|
|
477
|
+
process.exit(1);
|
|
312
478
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
socket.on("legion:update_token", async (data2) => {
|
|
482
|
+
try {
|
|
483
|
+
log.info("\u{1F504} Received permanent credentials. Persisting...");
|
|
484
|
+
await updateConfig({
|
|
485
|
+
token: data2.secret,
|
|
486
|
+
// secret -> token
|
|
487
|
+
id: data2.token_id
|
|
488
|
+
// token_id -> id
|
|
489
|
+
});
|
|
490
|
+
log.info("\u2705 Config updated successfully. Reconnecting with new token...");
|
|
491
|
+
await onTokenRotation();
|
|
492
|
+
} catch (error) {
|
|
493
|
+
log.error("\u274C Failed to update config", { error });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
socket.on("server:ping", (data2) => {
|
|
497
|
+
log.debug("\u{1F4E9} Ping from server", data2);
|
|
498
|
+
socket.emit("legion:pong", { ts: Date.now() });
|
|
499
|
+
});
|
|
500
|
+
socket.on("reconnect", (attemptNumber) => {
|
|
501
|
+
log.info("\u{1F504} Reconnected to server", { attempt: attemptNumber });
|
|
502
|
+
});
|
|
503
|
+
socket.on("reconnect_attempt", (attemptNumber) => {
|
|
504
|
+
log.debug("\u{1F504} Reconnection attempt", { attempt: attemptNumber });
|
|
505
|
+
});
|
|
506
|
+
socket.on("reconnect_error", (error) => {
|
|
507
|
+
log.error("\u{1F504} Reconnection error", { message: error.message });
|
|
508
|
+
});
|
|
509
|
+
socket.on("reconnect_failed", () => {
|
|
510
|
+
log.error("\u{1F504} Reconnection failed after all attempts");
|
|
511
|
+
process.exit(1);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async function main() {
|
|
515
|
+
let log = null;
|
|
516
|
+
const safeLog = {
|
|
517
|
+
info: (message, extra) => {
|
|
518
|
+
if (log) log.info(message, extra);
|
|
519
|
+
else console.log(message, extra);
|
|
520
|
+
},
|
|
521
|
+
error: (message, extra) => {
|
|
522
|
+
if (log) log.error(message, extra);
|
|
523
|
+
else console.error(message, extra);
|
|
524
|
+
},
|
|
525
|
+
warn: (message, extra) => {
|
|
526
|
+
if (log) log.warn(message, extra);
|
|
527
|
+
else console.warn(message, extra);
|
|
528
|
+
},
|
|
529
|
+
debug: (message, extra) => {
|
|
530
|
+
if (log) log.debug(message, extra);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
try {
|
|
534
|
+
log = Log.create({ service: "legion" });
|
|
535
|
+
const args = parseArgs();
|
|
536
|
+
if (args.command === "login") {
|
|
537
|
+
try {
|
|
538
|
+
await handleDeviceLogin(log);
|
|
539
|
+
process.exit(0);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
log.error("\u274C Device login failed", { error });
|
|
316
542
|
process.exit(1);
|
|
317
543
|
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
544
|
+
}
|
|
545
|
+
if (args.token) {
|
|
546
|
+
log.info("\u{1F511} Saving token from command line...");
|
|
547
|
+
try {
|
|
548
|
+
const defaultServerUrl = process.env.TANUKI_SERVER_URL || "wss://tanuki.sabw.ru";
|
|
549
|
+
const existingConfig = await loadConfig();
|
|
550
|
+
await saveConfig({
|
|
551
|
+
token: args.token,
|
|
552
|
+
serverUrl: existingConfig?.serverUrl || defaultServerUrl,
|
|
553
|
+
...existingConfig?.id ? { id: existingConfig.id } : {}
|
|
554
|
+
});
|
|
555
|
+
log.info("\u2705 Token saved successfully");
|
|
556
|
+
} catch (error) {
|
|
557
|
+
log.error("\u274C Failed to save token", { error });
|
|
323
558
|
process.exit(1);
|
|
324
559
|
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
560
|
+
}
|
|
561
|
+
checkForUpdates();
|
|
562
|
+
await Global.init();
|
|
563
|
+
const fingerprint = await getSystemFingerprint();
|
|
564
|
+
log.info(`\u{1F6E1}\uFE0F Legion v${version} starting...`);
|
|
565
|
+
let config2 = await getConfig();
|
|
566
|
+
log.info("\u{1F517} Connecting to server", { serverUrl: config2.serverUrl });
|
|
567
|
+
let currentSocket;
|
|
568
|
+
let isTokenRotation = false;
|
|
569
|
+
let handleTokenRotation;
|
|
570
|
+
let handleReconnect;
|
|
571
|
+
handleReconnect = async () => {
|
|
572
|
+
if (currentSocket) {
|
|
573
|
+
currentSocket.io.opts.reconnection = false;
|
|
574
|
+
currentSocket.disconnect();
|
|
575
|
+
currentSocket.removeAllListeners();
|
|
576
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
577
|
+
}
|
|
578
|
+
const newConfig = await getConfig();
|
|
579
|
+
log.info("\u{1F504} Reconnecting...");
|
|
580
|
+
currentSocket = createSocket(newConfig);
|
|
581
|
+
setupSocketHandlers(currentSocket, log, fingerprint, version, handleTokenRotation, handleReconnect);
|
|
582
|
+
config2 = newConfig;
|
|
583
|
+
};
|
|
584
|
+
handleTokenRotation = async () => {
|
|
585
|
+
isTokenRotation = true;
|
|
586
|
+
await handleReconnect();
|
|
587
|
+
isTokenRotation = false;
|
|
588
|
+
};
|
|
589
|
+
currentSocket = createSocket(config2);
|
|
590
|
+
setupSocketHandlers(currentSocket, log, fingerprint, version, handleTokenRotation, handleReconnect);
|
|
343
591
|
process.stdin.resume();
|
|
344
592
|
const shutdown = () => {
|
|
345
|
-
|
|
346
|
-
|
|
593
|
+
log.info("\u{1F6D1} Shutting down...");
|
|
594
|
+
currentSocket.disconnect();
|
|
347
595
|
process.exit(0);
|
|
348
596
|
};
|
|
349
597
|
process.on("SIGINT", shutdown);
|
|
350
598
|
process.on("SIGTERM", shutdown);
|
|
351
599
|
process.on("unhandledRejection", (reason) => {
|
|
352
|
-
|
|
600
|
+
log.error("Unhandled rejection", { reason });
|
|
353
601
|
});
|
|
354
602
|
process.on("uncaughtException", (error) => {
|
|
355
|
-
|
|
603
|
+
log.error("Uncaught exception", { error: error.message, stack: error.stack });
|
|
356
604
|
shutdown();
|
|
357
605
|
});
|
|
358
606
|
} catch (error) {
|
|
359
607
|
if (error instanceof Error) {
|
|
360
|
-
|
|
608
|
+
safeLog.error("Failed to start Legion", {
|
|
361
609
|
message: error.message,
|
|
362
610
|
stack: error.stack
|
|
363
611
|
});
|
|
@@ -366,7 +614,7 @@ async function main() {
|
|
|
366
614
|
console.error(" - Set LEGION_TOKEN environment variable, or");
|
|
367
615
|
console.error(" - Create ~/.tanuki/config.json with your token");
|
|
368
616
|
} else {
|
|
369
|
-
|
|
617
|
+
safeLog.error("Failed to start Legion", { error });
|
|
370
618
|
console.error("\n\u274C Unknown error occurred");
|
|
371
619
|
}
|
|
372
620
|
process.exit(1);
|