lg-dev-manager-bridge 1.0.2 → 1.0.4
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/README.md
CHANGED
|
@@ -188,6 +188,12 @@ pnpm app # the standalone app for this computer → apps/bridge/relea
|
|
|
188
188
|
`tools/mock-tv` fakes webOS TVs (SSH, luna-send, key server, logs) so everything can be tested without a TV.
|
|
189
189
|
Contributors, human or AI: read [AGENTS.md](https://github.com/Scoty/lg-dev-manager/blob/main/AGENTS.md) and [PLAN.md](https://github.com/Scoty/lg-dev-manager/blob/main/PLAN.md) first.
|
|
190
190
|
|
|
191
|
+
## 🙋 Problems and ideas
|
|
192
|
+
|
|
193
|
+
[Open an issue](https://github.com/Scoty/lg-dev-manager/issues/new/choose): there are forms for bugs, connection
|
|
194
|
+
problems and feature requests that ask for what helps track a problem down. Security problems: please report them
|
|
195
|
+
privately (see the [security policy](https://github.com/Scoty/lg-dev-manager/security/policy)).
|
|
196
|
+
|
|
191
197
|
## 📄 License
|
|
192
198
|
|
|
193
199
|
[Apache-2.0](https://github.com/Scoty/lg-dev-manager/blob/main/LICENSE). You may use, change and share the app and its code freely, including commercially, as long
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
-
import { randomBytes } from "crypto";
|
|
5
|
-
import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
6
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
-
import { homedir } from "os";
|
|
8
|
-
import { join } from "path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
// ../../packages/protocol/src/envelope.ts
|
|
11
11
|
import { z } from "zod";
|
|
@@ -124,7 +124,9 @@ var AppsErrorCodes = {
|
|
|
124
124
|
FileTooLarge: "file_too_large",
|
|
125
125
|
TransferFailed: "transfer_failed",
|
|
126
126
|
/** Another app with this id is installed outside the developer partition (e.g. from the LG Content Store). */
|
|
127
|
-
Conflict: "app_conflict"
|
|
127
|
+
Conflict: "app_conflict",
|
|
128
|
+
/** The TV is in standby: apps can be installed, updated and removed, but a launch would show nothing. */
|
|
129
|
+
TvOff: "tv_off"
|
|
128
130
|
};
|
|
129
131
|
|
|
130
132
|
// ../../packages/protocol/src/console.ts
|
|
@@ -335,6 +337,7 @@ var LitefinErrorCodes = {
|
|
|
335
337
|
var PROTOCOL_VERSION = 8;
|
|
336
338
|
var base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "Not base64");
|
|
337
339
|
var DEFAULT_BRIDGE_PORT = 5199;
|
|
340
|
+
var TvPowerState = z9.enum(["on", "screenOff", "standby", "unknown"]);
|
|
338
341
|
var Methods = {
|
|
339
342
|
/** First message on every connection. Authenticates with the pairing token. */
|
|
340
343
|
"system.hello": {
|
|
@@ -404,6 +407,15 @@ var Methods = {
|
|
|
404
407
|
socName: z9.string().optional()
|
|
405
408
|
})
|
|
406
409
|
},
|
|
410
|
+
/**
|
|
411
|
+
* Is the TV on? `com.webos.service.tvpower/power/getPowerState`: `on` (also with the screen saver), `screenOff` (on,
|
|
412
|
+
* picture turned off), `standby` (looks off — the system still runs, so installs work, but nothing can be shown),
|
|
413
|
+
* `unknown` (the TV didn't say; treat as on). `raw` is the TV's own state text.
|
|
414
|
+
*/
|
|
415
|
+
"device.power": {
|
|
416
|
+
params: z9.object({ device: DeviceTarget }),
|
|
417
|
+
result: z9.object({ state: TvPowerState, raw: z9.string().optional() })
|
|
418
|
+
},
|
|
407
419
|
/** `df` of a folder (KiB; default the developer partition). Null if the TV gave no usable answer. */
|
|
408
420
|
"device.storage": {
|
|
409
421
|
params: z9.object({ device: DeviceTarget, path: RemotePath.optional() }),
|
|
@@ -743,9 +755,9 @@ var REPO_URL = "https://github.com/Scoty/lg-dev-manager";
|
|
|
743
755
|
var RELEASES_URL = `${REPO_URL}/releases/latest`;
|
|
744
756
|
|
|
745
757
|
// src/standalone.ts
|
|
746
|
-
import { existsSync } from "fs";
|
|
747
|
-
import { fileURLToPath } from "url";
|
|
748
|
-
import * as sea from "sea";
|
|
758
|
+
import { existsSync } from "node:fs";
|
|
759
|
+
import { fileURLToPath } from "node:url";
|
|
760
|
+
import * as sea from "node:sea";
|
|
749
761
|
function isStandalone() {
|
|
750
762
|
try {
|
|
751
763
|
return sea.isSea();
|
|
@@ -868,7 +880,7 @@ function parseArgs(argv) {
|
|
|
868
880
|
}
|
|
869
881
|
|
|
870
882
|
// src/server.ts
|
|
871
|
-
import { createServer } from "http";
|
|
883
|
+
import { createServer } from "node:http";
|
|
872
884
|
import { WebSocketServer } from "ws";
|
|
873
885
|
|
|
874
886
|
// src/rpc/errors.ts
|
|
@@ -886,16 +898,16 @@ var RpcError = class extends Error {
|
|
|
886
898
|
};
|
|
887
899
|
|
|
888
900
|
// src/rpc/handlers.ts
|
|
889
|
-
import { StringDecoder as StringDecoder3 } from "string_decoder";
|
|
890
|
-
import { timingSafeEqual } from "crypto";
|
|
891
|
-
import { platform } from "os";
|
|
892
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
901
|
+
import { StringDecoder as StringDecoder3 } from "node:string_decoder";
|
|
902
|
+
import { timingSafeEqual } from "node:crypto";
|
|
903
|
+
import { platform } from "node:os";
|
|
904
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
893
905
|
|
|
894
906
|
// src/version.ts
|
|
895
|
-
var BRIDGE_VERSION = "1.0.
|
|
907
|
+
var BRIDGE_VERSION = "1.0.4";
|
|
896
908
|
|
|
897
909
|
// src/ssh/logged.ts
|
|
898
|
-
import { randomUUID } from "crypto";
|
|
910
|
+
import { randomUUID } from "node:crypto";
|
|
899
911
|
var MAX_LOG_OUTPUT = 8 * 1024;
|
|
900
912
|
var targetOf = (t) => `${t.username}@${t.host}:${t.port}`;
|
|
901
913
|
function summarize(stdout, stderr) {
|
|
@@ -1000,16 +1012,16 @@ var LoggedSsh = class {
|
|
|
1000
1012
|
};
|
|
1001
1013
|
|
|
1002
1014
|
// src/devices/scan.ts
|
|
1003
|
-
import { createSocket } from "dgram";
|
|
1004
|
-
import { request } from "http";
|
|
1005
|
-
import { networkInterfaces } from "os";
|
|
1015
|
+
import { createSocket } from "node:dgram";
|
|
1016
|
+
import { request } from "node:http";
|
|
1017
|
+
import { networkInterfaces } from "node:os";
|
|
1006
1018
|
|
|
1007
1019
|
// src/devices/ports.ts
|
|
1008
|
-
import { connect as connect2 } from "net";
|
|
1020
|
+
import { connect as connect2 } from "node:net";
|
|
1009
1021
|
|
|
1010
1022
|
// src/devices/webos.ts
|
|
1011
1023
|
import WebSocket from "ws";
|
|
1012
|
-
import { connect } from "net";
|
|
1024
|
+
import { connect } from "node:net";
|
|
1013
1025
|
var PROBE_TIMEOUT_MS = 1500;
|
|
1014
1026
|
function probeSsap(host, port, secure, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
1015
1027
|
return new Promise((resolve2) => {
|
|
@@ -1273,7 +1285,7 @@ async function scanNetwork(opts = {}) {
|
|
|
1273
1285
|
}
|
|
1274
1286
|
|
|
1275
1287
|
// src/ssh/luna.ts
|
|
1276
|
-
import { StringDecoder } from "string_decoder";
|
|
1288
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1277
1289
|
function shellQuote(value) {
|
|
1278
1290
|
return value.split("'").map((s) => `'${s}'`).join("\\'");
|
|
1279
1291
|
}
|
|
@@ -1394,7 +1406,7 @@ async function lunaSubscribe(pool, device, uri, params, onMessage, opts = {}) {
|
|
|
1394
1406
|
}
|
|
1395
1407
|
|
|
1396
1408
|
// src/devices/keys.ts
|
|
1397
|
-
import { createHash } from "crypto";
|
|
1409
|
+
import { createHash } from "node:crypto";
|
|
1398
1410
|
import ssh2 from "ssh2";
|
|
1399
1411
|
var { utils } = ssh2;
|
|
1400
1412
|
function verifyKey(privateKey, passphrase) {
|
|
@@ -1415,7 +1427,7 @@ function verifyKey(privateKey, passphrase) {
|
|
|
1415
1427
|
}
|
|
1416
1428
|
|
|
1417
1429
|
// src/devices/keyserver.ts
|
|
1418
|
-
import { request as request2 } from "http";
|
|
1430
|
+
import { request as request2 } from "node:http";
|
|
1419
1431
|
var MAX_KEY_RESPONSE = 65536;
|
|
1420
1432
|
var TIMEOUT_MS = 1e4;
|
|
1421
1433
|
var MAX_REDIRECTS = 5;
|
|
@@ -1495,7 +1507,7 @@ async function fetchKey(host, port = KEY_SERVER_PORT) {
|
|
|
1495
1507
|
}
|
|
1496
1508
|
|
|
1497
1509
|
// src/devices/info.ts
|
|
1498
|
-
import { createHash as createHash2 } from "crypto";
|
|
1510
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1499
1511
|
import ssh22 from "ssh2";
|
|
1500
1512
|
|
|
1501
1513
|
// src/ssh/transfer.ts
|
|
@@ -1777,9 +1789,42 @@ async function renewDevMode(pool, device) {
|
|
|
1777
1789
|
await lunaCall(pool, device, "luna://com.webos.applicationManager/launch", { id: "com.palmdts.devmode", subscribe: false, params: { extend: true } }, true);
|
|
1778
1790
|
}
|
|
1779
1791
|
|
|
1792
|
+
// src/devices/power.ts
|
|
1793
|
+
var GET_POWER_STATE = "luna://com.webos.service.tvpower/power/getPowerState";
|
|
1794
|
+
var TURN_ON_SCREEN = "luna://com.webos.service.tvpower/power/turnOnScreen";
|
|
1795
|
+
function powerStateOf(msg) {
|
|
1796
|
+
const state = typeof msg.state === "string" ? msg.state : "";
|
|
1797
|
+
const processing = typeof msg.processing === "string" ? msg.processing : "";
|
|
1798
|
+
if (/power ?off|suspend|standby/i.test(processing)) return "standby";
|
|
1799
|
+
if (state === "Active" || state === "Screen Saver") return "on";
|
|
1800
|
+
if (state === "Screen Off") return "screenOff";
|
|
1801
|
+
if (/standby|suspend|power ?off/i.test(state)) return "standby";
|
|
1802
|
+
return "unknown";
|
|
1803
|
+
}
|
|
1804
|
+
async function tvPower(pool, device) {
|
|
1805
|
+
for (const pub of device.username === "root" ? [true, false] : [true]) {
|
|
1806
|
+
try {
|
|
1807
|
+
const msg = await lunaCall(pool, device, GET_POWER_STATE, {}, pub);
|
|
1808
|
+
return { state: powerStateOf(msg), raw: [msg.state, msg.processing].filter((v) => typeof v === "string").join(" \xB7 ") || void 0 };
|
|
1809
|
+
} catch {
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return { state: "unknown" };
|
|
1813
|
+
}
|
|
1814
|
+
async function turnOnScreen(pool, device) {
|
|
1815
|
+
for (const pub of device.username === "root" ? [true, false] : [true]) {
|
|
1816
|
+
try {
|
|
1817
|
+
await lunaCall(pool, device, TURN_ON_SCREEN, {}, pub);
|
|
1818
|
+
return true;
|
|
1819
|
+
} catch {
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1780
1825
|
// src/apps/apps.ts
|
|
1781
|
-
import { createHash as createHash3 } from "crypto";
|
|
1782
|
-
import { posix } from "path";
|
|
1826
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1827
|
+
import { posix } from "node:path";
|
|
1783
1828
|
|
|
1784
1829
|
// src/ssh/serve.ts
|
|
1785
1830
|
var HEADER_LIMIT = 16 * 1024;
|
|
@@ -1868,7 +1913,7 @@ Connection: close\r
|
|
|
1868
1913
|
}
|
|
1869
1914
|
|
|
1870
1915
|
// src/apps/ipk.ts
|
|
1871
|
-
import { gunzipSync } from "zlib";
|
|
1916
|
+
import { gunzipSync } from "node:zlib";
|
|
1872
1917
|
function readIpkControl(ipk) {
|
|
1873
1918
|
try {
|
|
1874
1919
|
const control = arMember(ipk, /^control\.tar\.gz\/?$/);
|
|
@@ -1942,7 +1987,16 @@ async function listApps(pool, device) {
|
|
|
1942
1987
|
});
|
|
1943
1988
|
}
|
|
1944
1989
|
async function launchApp(pool, device, id, params) {
|
|
1990
|
+
const power = await tvPower(pool, device);
|
|
1991
|
+
if (power.state === "standby") {
|
|
1992
|
+
throw new RpcError(
|
|
1993
|
+
AppsErrorCodes.TvOff,
|
|
1994
|
+
"The TV is off (in standby). Turn it on with the remote, then launch the app. Installing, updating and removing apps work while it\u2019s off.",
|
|
1995
|
+
power.raw ? `getPowerState: ${power.raw}` : void 0
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1945
1998
|
await lunaCall(pool, device, "luna://com.webos.applicationManager/launch", { id, subscribe: false, params }, true);
|
|
1999
|
+
if (power.state === "screenOff") await turnOnScreen(pool, device);
|
|
1946
2000
|
}
|
|
1947
2001
|
async function appIcon(pool, device, path) {
|
|
1948
2002
|
const mime = ICON_TYPES[posix.extname(path).toLowerCase()];
|
|
@@ -2174,7 +2228,7 @@ async function installDownloadable(pool, device, pkg, download, progress) {
|
|
|
2174
2228
|
}
|
|
2175
2229
|
|
|
2176
2230
|
// src/files/files.ts
|
|
2177
|
-
import { posix as posix2 } from "path";
|
|
2231
|
+
import { posix as posix2 } from "node:path";
|
|
2178
2232
|
var S_IFMT = 61440;
|
|
2179
2233
|
var TYPES = {
|
|
2180
2234
|
16384: "d",
|
|
@@ -2413,9 +2467,9 @@ async function removePath(pool, device, path) {
|
|
|
2413
2467
|
}
|
|
2414
2468
|
|
|
2415
2469
|
// src/debug/debug.ts
|
|
2416
|
-
import { posix as posix3 } from "path";
|
|
2417
|
-
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
2418
|
-
import { createGunzip } from "zlib";
|
|
2470
|
+
import { posix as posix3 } from "node:path";
|
|
2471
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
2472
|
+
import { createGunzip } from "node:zlib";
|
|
2419
2473
|
var requireRoot = (device, what) => {
|
|
2420
2474
|
if (device.username !== "root") {
|
|
2421
2475
|
throw new RpcError(DeviceErrorCodes.WrongLogin, `${what} needs a rooted TV (logged in as root).`);
|
|
@@ -2681,6 +2735,8 @@ var handlers = {
|
|
|
2681
2735
|
},
|
|
2682
2736
|
"device.info": ({ device, quiet }, session, ctx) => deviceInfo(sshFor(session, ctx, quiet), device),
|
|
2683
2737
|
"device.storage": ({ device, path }, session, ctx) => storageInfo(sshFor(session, ctx), device, path),
|
|
2738
|
+
// Polled by the apps pages: a background read in the console.
|
|
2739
|
+
"device.power": ({ device }, session, ctx) => tvPower(sshFor(session, ctx, true), device),
|
|
2684
2740
|
"device.generateKey": ({ comment }) => generateKey(comment),
|
|
2685
2741
|
// The token is read through the plain pool so it never appears in the console; the LG request is traced
|
|
2686
2742
|
// with the token blanked out.
|
|
@@ -2924,8 +2980,8 @@ async function dispatch(raw, session, ctx) {
|
|
|
2924
2980
|
}
|
|
2925
2981
|
|
|
2926
2982
|
// src/http/static.ts
|
|
2927
|
-
import { createReadStream, statSync } from "fs";
|
|
2928
|
-
import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "path";
|
|
2983
|
+
import { createReadStream, statSync } from "node:fs";
|
|
2984
|
+
import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "node:path";
|
|
2929
2985
|
var MIME = {
|
|
2930
2986
|
".html": "text/html; charset=utf-8",
|
|
2931
2987
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -3019,7 +3075,7 @@ function serveStatic(root, req, res) {
|
|
|
3019
3075
|
}
|
|
3020
3076
|
|
|
3021
3077
|
// src/ssh/pool.ts
|
|
3022
|
-
import { createHash as createHash4 } from "crypto";
|
|
3078
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3023
3079
|
import ssh23 from "ssh2";
|
|
3024
3080
|
var { Client } = ssh23;
|
|
3025
3081
|
var IDLE_MS = 12e4;
|
|
@@ -3347,7 +3403,7 @@ var SshPool = class {
|
|
|
3347
3403
|
};
|
|
3348
3404
|
|
|
3349
3405
|
// src/rpc/uploads.ts
|
|
3350
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
3406
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3351
3407
|
var UploadBudget = class {
|
|
3352
3408
|
constructor(limit = MAX_UPLOAD_BYTES) {
|
|
3353
3409
|
this.limit = limit;
|
|
@@ -3450,13 +3506,13 @@ var UploadStore = class {
|
|
|
3450
3506
|
};
|
|
3451
3507
|
|
|
3452
3508
|
// src/repo/repo.ts
|
|
3453
|
-
import { createHash as createHash5 } from "crypto";
|
|
3454
|
-
import { lookup } from "dns/promises";
|
|
3455
|
-
import { lookup as dnsLookup } from "dns";
|
|
3456
|
-
import { request as httpRequest } from "http";
|
|
3457
|
-
import { request as httpsRequest } from "https";
|
|
3458
|
-
import { BlockList, isIP } from "net";
|
|
3459
|
-
import { Readable } from "stream";
|
|
3509
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3510
|
+
import { lookup } from "node:dns/promises";
|
|
3511
|
+
import { lookup as dnsLookup } from "node:dns";
|
|
3512
|
+
import { request as httpRequest } from "node:http";
|
|
3513
|
+
import { request as httpsRequest } from "node:https";
|
|
3514
|
+
import { BlockList, isIP } from "node:net";
|
|
3515
|
+
import { Readable } from "node:stream";
|
|
3460
3516
|
var untraced = (_t, _c, fn) => fn();
|
|
3461
3517
|
var CACHE_MS = 5 * 6e4;
|
|
3462
3518
|
var JSON_MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -4015,7 +4071,7 @@ function lookup2(entries, tag, variant) {
|
|
|
4015
4071
|
}
|
|
4016
4072
|
|
|
4017
4073
|
// src/shell/shells.ts
|
|
4018
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
4074
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4019
4075
|
var MAX_SHELLS = 8;
|
|
4020
4076
|
var FLUSH_MS2 = 8;
|
|
4021
4077
|
var FLUSH_BYTES = 64 * 1024;
|
|
@@ -4296,7 +4352,7 @@ function startServer(config, pool = new SshPool(), repo = new RepoClient(config.
|
|
|
4296
4352
|
}
|
|
4297
4353
|
|
|
4298
4354
|
// src/open.ts
|
|
4299
|
-
import { spawn } from "child_process";
|
|
4355
|
+
import { spawn } from "node:child_process";
|
|
4300
4356
|
function openInBrowser(url2) {
|
|
4301
4357
|
if (!/^https:\/\/[a-z0-9.-]+\/?$/i.test(url2)) return;
|
|
4302
4358
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url2]] : ["xdg-open", [url2]];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lg-dev-manager-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Local bridge for LG Dev Manager: lets the web app at lg.scoty.uk (or its local copy) manage LG webOS TVs over SSH",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Scoty (https://github.com/Scoty)",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"tsx": "^4.20.0",
|
|
52
52
|
"typescript": "^5.9.0",
|
|
53
53
|
"vitest": "^4.1.11",
|
|
54
|
-
"@lgdm/mock-tv": "1.0.
|
|
55
|
-
"@lgdm/protocol": "1.0.
|
|
54
|
+
"@lgdm/mock-tv": "1.0.4",
|
|
55
|
+
"@lgdm/protocol": "1.0.4"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"dev": "tsx watch src/cli.ts --dev",
|