lg-dev-manager-bridge 1.0.1 → 1.0.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/README.md
CHANGED
|
@@ -24,20 +24,52 @@ A web rebuild of [webosbrew/dev-manager-desktop](https://github.com/webosbrew/de
|
|
|
24
24
|
|
|
25
25
|
## 🚀 Get started
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
The app needs a small helper on your computer, **the bridge** (why: [How it works](#how-it-works)). There are three
|
|
28
|
+
ways to run it. It's the same bridge every time, so pick whichever suits you:
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
| | Needs | How |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 🖥️ **The app** | nothing | Download it from the [latest release](https://github.com/Scoty/lg-dev-manager/releases/latest) and open it |
|
|
33
|
+
| 📦 **npx** | Node.js 22.12+ | `npx lg-dev-manager-bridge@latest` in a terminal |
|
|
34
|
+
| 🛠️ **From the source code** | Node.js 22.12+ | [Clone, build and run](#run-from-source) |
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
npx lg-dev-manager-bridge@latest
|
|
35
|
-
```
|
|
36
|
+
### 🖥️ Option 1: the app — no Node.js needed
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
One file with everything inside. Download the one for your computer from the
|
|
39
|
+
[latest release](https://github.com/Scoty/lg-dev-manager/releases/latest):
|
|
40
|
+
|
|
41
|
+
| Computer | File |
|
|
42
|
+
|---|---|
|
|
43
|
+
| 🪟 Windows | `lg-dev-manager-bridge-<version>-windows-x64.exe` — just run it (also works on Windows on ARM) |
|
|
44
|
+
| 🍎 Mac with Apple Silicon (M1 and newer) | `…-macos-arm64.dmg` — open it, then double-click `lg-dev-manager-bridge` |
|
|
45
|
+
| 🍎 Mac with Intel | `…-macos-x64.dmg` |
|
|
46
|
+
| 🐧 Linux (x64 / ARM64) | `…-linux-x64.tar.gz` / `…-linux-arm64.tar.gz` — `tar -xzf …`, then `./lg-dev-manager-bridge` |
|
|
47
|
+
|
|
48
|
+
A window opens with the **pairing token**, and https://lg.scoty.uk opens in your browser. Keep the window open while you
|
|
49
|
+
use the app; closing it stops the bridge.
|
|
50
|
+
|
|
51
|
+
> 🔐 **The first time:** the app isn't signed by Apple or Microsoft (that costs a yearly fee), so your computer asks
|
|
52
|
+
> before running it. **Windows**: "Windows protected your PC" → **More info** → **Run anyway**. **macOS**: "Apple
|
|
53
|
+
> could not verify…" → **Done**, then **System Settings → Privacy & Security** → **Open Anyway**. You only do this
|
|
54
|
+
> once. It's the same code as the npm package, built and tested by GitHub Actions in public. Each release lists
|
|
55
|
+
> SHA-256 checksums.
|
|
56
|
+
|
|
57
|
+
### 📦 Option 2: npx
|
|
58
|
+
|
|
59
|
+
**You need [Node.js](https://nodejs.org) 22.12 or newer** (any current LTS — 22 or 24). `node --version` shows
|
|
60
|
+
yours. In a terminal:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx lg-dev-manager-bridge@latest
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It prints a **pairing token**. Keep the terminal open while you use the app (Ctrl+C stops it).
|
|
67
|
+
|
|
68
|
+
### 🔗 Then, whichever way you chose
|
|
69
|
+
|
|
70
|
+
1. Open **https://lg.scoty.uk**, or the local page the bridge serves at **http://localhost:5199** — they are the
|
|
39
71
|
same app. On the **Bridge** page, paste the token.
|
|
40
|
-
|
|
72
|
+
2. On **Devices → Add a TV**, pick your TV from the scan. Rooted TVs log in as root (Homebrew Channel's SSH server);
|
|
41
73
|
other TVs use the Developer Mode app and its passphrase.
|
|
42
74
|
|
|
43
75
|
🍎 On macOS, the first time the bridge looks for your TV, the system asks whether your terminal may find devices on the
|
|
@@ -49,6 +81,8 @@ bridge. 🏠 **http://localhost:5199** is served from your computer by the bridg
|
|
|
49
81
|
website can't be reached. Each keeps its own list of TVs (browsers store them per site); use **Export backup** / **Import backup** on
|
|
50
82
|
the Devices page to move them.
|
|
51
83
|
|
|
84
|
+
<a id="how-it-works"></a>
|
|
85
|
+
|
|
52
86
|
## 🔧 How it works
|
|
53
87
|
|
|
54
88
|
Browsers can't open SSH connections, so the app comes in two parts:
|
|
@@ -57,7 +91,7 @@ Browsers can't open SSH connections, so the app comes in two parts:
|
|
|
57
91
|
Browser (lg.scoty.uk or localhost:5199) Your computer Your TV
|
|
58
92
|
┌──────────────────────────────────┐ WebSocket ┌────────────────────┐ SSH / SFTP ┌──────────────┐
|
|
59
93
|
│ Web app — your TVs, keys and │ ────────────▶ │ Bridge │ ─────────────▶ │ webOS │
|
|
60
|
-
│ passwords are stored only here │ 127.0.0.1 │ (
|
|
94
|
+
│ passwords are stored only here │ 127.0.0.1 │ (app or npx) │ │ (port 22 or │
|
|
61
95
|
└──────────────────────────────────┘ + token └────────────────────┘ │ 9922) │
|
|
62
96
|
└──────────────┘
|
|
63
97
|
```
|
|
@@ -71,8 +105,9 @@ Browsers can't open SSH connections, so the app comes in two parts:
|
|
|
71
105
|
- 📦 **Homebrew repository and Litefin releases** are fetched by the bridge, so the site itself talks to no one but your
|
|
72
106
|
bridge. Downloads are checked against their published SHA-256 whenever one is published.
|
|
73
107
|
|
|
74
|
-
🔄 The bridge tells the site its version; when a newer one is out, the app says so
|
|
75
|
-
|
|
108
|
+
🔄 The bridge tells the site its version; when a newer one is out, the app says so and how to update: download the
|
|
109
|
+
new app from the [releases](https://github.com/Scoty/lg-dev-manager/releases/latest), or run
|
|
110
|
+
`npx lg-dev-manager-bridge@latest` again. Your pairing and TVs stay as they are.
|
|
76
111
|
|
|
77
112
|
## ✨ What it does
|
|
78
113
|
|
|
@@ -109,9 +144,11 @@ downloading the right IPK by hand and installing it through the desktop webOS De
|
|
|
109
144
|
— through Homebrew Channel on a rooted TV, or the Developer Mode installer otherwise. All builds are the same app,
|
|
110
145
|
so switching builds or versions simply replaces it.
|
|
111
146
|
|
|
147
|
+
<a id="run-from-source"></a>
|
|
148
|
+
|
|
112
149
|
## 🛠️ Run the bridge from the source code
|
|
113
150
|
|
|
114
|
-
|
|
151
|
+
The third way: run it from a clone. It's the same bridge, and it serves the local page too:
|
|
115
152
|
|
|
116
153
|
```bash
|
|
117
154
|
git clone https://github.com/Scoty/lg-dev-manager.git
|
|
@@ -125,7 +162,7 @@ pnpm bridge # same as: node apps/bridge/dist/cli.js
|
|
|
125
162
|
This needs Node.js 22.12 or newer, the same as npx (the project itself is developed on Node 24, see `.nvmrc`).
|
|
126
163
|
To update: `git pull`, `pnpm install`, `pnpm build`, then `pnpm bridge`.
|
|
127
164
|
|
|
128
|
-
Bridge options (`
|
|
165
|
+
Bridge options (`--help`; with the app, run it from a terminal: `lg-dev-manager-bridge --help`):
|
|
129
166
|
|
|
130
167
|
| Option | |
|
|
131
168
|
|---|---|
|
|
@@ -133,6 +170,8 @@ Bridge options (`npx lg-dev-manager-bridge --help`):
|
|
|
133
170
|
| `--no-ui` | Don't serve the local page |
|
|
134
171
|
| `--allow-origin <url>` | Also accept another origin, e.g. a self-hosted copy of the web app |
|
|
135
172
|
| `--reset-token` | Make a new pairing token (un-pairs every browser) |
|
|
173
|
+
| `--no-open` | The app only: don't open lg.scoty.uk in the browser when it starts |
|
|
174
|
+
| `--license` | Show the licenses of the bridge and everything it includes |
|
|
136
175
|
|
|
137
176
|
## 🧪 Development
|
|
138
177
|
|
|
@@ -143,11 +182,18 @@ pnpm test # unit and integration tests (against a mock TV)
|
|
|
143
182
|
pnpm e2e # Playwright: built app + real bridge + mock TVs + fake Homebrew repo
|
|
144
183
|
pnpm dev:rig # the same rig to try by hand: http://127.0.0.1:5299, token e2e-token
|
|
145
184
|
pnpm lint && pnpm typecheck
|
|
185
|
+
pnpm app # the standalone app for this computer → apps/bridge/release (after pnpm build)
|
|
146
186
|
```
|
|
147
187
|
|
|
148
188
|
`tools/mock-tv` fakes webOS TVs (SSH, luna-send, key server, logs) so everything can be tested without a TV.
|
|
149
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.
|
|
150
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
|
+
|
|
151
197
|
## 📄 License
|
|
152
198
|
|
|
153
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, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
6
|
-
import { fileURLToPath } 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";
|
|
@@ -346,7 +346,9 @@ var Methods = {
|
|
|
346
346
|
result: z9.object({
|
|
347
347
|
protocolVersion: z9.number().int(),
|
|
348
348
|
bridgeVersion: z9.string(),
|
|
349
|
-
platform: z9.string()
|
|
349
|
+
platform: z9.string(),
|
|
350
|
+
/** How the bridge was installed, so the site can say how to update it (bridges before 1.0.2 don't say). */
|
|
351
|
+
distribution: z9.enum(["app", "npm", "source"]).optional()
|
|
350
352
|
})
|
|
351
353
|
},
|
|
352
354
|
"system.ping": {
|
|
@@ -737,6 +739,54 @@ var UNAUTHENTICATED_METHODS = ["system.hello"];
|
|
|
737
739
|
var PUBLIC_WEB_ORIGIN = "https://lg.scoty.uk";
|
|
738
740
|
var DEFAULT_ALLOWED_ORIGINS = [PUBLIC_WEB_ORIGIN];
|
|
739
741
|
var DEV_ALLOWED_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
|
742
|
+
var REPO_URL = "https://github.com/Scoty/lg-dev-manager";
|
|
743
|
+
var RELEASES_URL = `${REPO_URL}/releases/latest`;
|
|
744
|
+
|
|
745
|
+
// src/standalone.ts
|
|
746
|
+
import { existsSync } from "node:fs";
|
|
747
|
+
import { fileURLToPath } from "node:url";
|
|
748
|
+
import * as sea from "node:sea";
|
|
749
|
+
function isStandalone() {
|
|
750
|
+
try {
|
|
751
|
+
return sea.isSea();
|
|
752
|
+
} catch {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function distribution() {
|
|
757
|
+
if (isStandalone()) return "app";
|
|
758
|
+
try {
|
|
759
|
+
return existsSync(fileURLToPath(new URL("../src/", import.meta.url))) ? "source" : "npm";
|
|
760
|
+
} catch {
|
|
761
|
+
return "npm";
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
var WEB_MANIFEST_ASSET = "web-manifest.json";
|
|
765
|
+
function standaloneUi() {
|
|
766
|
+
if (!isStandalone()) return void 0;
|
|
767
|
+
try {
|
|
768
|
+
const names = JSON.parse(sea.getAsset(WEB_MANIFEST_ASSET, "utf8"));
|
|
769
|
+
if (!Array.isArray(names)) return void 0;
|
|
770
|
+
const files = /* @__PURE__ */ new Map();
|
|
771
|
+
for (const name of names) {
|
|
772
|
+
if (typeof name === "string") files.set(name, new Uint8Array(sea.getAsset(`web/${name}`)));
|
|
773
|
+
}
|
|
774
|
+
return files.has("index.html") ? files : void 0;
|
|
775
|
+
} catch {
|
|
776
|
+
return void 0;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function licenseText() {
|
|
780
|
+
if (!isStandalone()) {
|
|
781
|
+
return `Apache-2.0. See the LICENSE and NOTICE files next to this program, or https://github.com/Scoty/lg-dev-manager/blob/main/LICENSE
|
|
782
|
+
`;
|
|
783
|
+
}
|
|
784
|
+
const rule = `
|
|
785
|
+
${"=".repeat(78)}
|
|
786
|
+
|
|
787
|
+
`;
|
|
788
|
+
return ["LICENSE", "NOTICE", "THIRD-PARTY-LICENSES.txt"].map((f) => sea.getAsset(f, "utf8").trim()).join(rule) + "\n";
|
|
789
|
+
}
|
|
740
790
|
|
|
741
791
|
// src/config.ts
|
|
742
792
|
var stateDir = () => process.env.LGDM_STATE_DIR ?? join(homedir(), ".lg-dev-manager");
|
|
@@ -769,9 +819,14 @@ function loadOrCreateToken(reset = false) {
|
|
|
769
819
|
return token;
|
|
770
820
|
}
|
|
771
821
|
function bundledUi() {
|
|
822
|
+
const packed = standaloneUi();
|
|
823
|
+
if (packed) return packed;
|
|
772
824
|
for (const rel of ["../web/", "../../web/dist/"]) {
|
|
773
|
-
|
|
774
|
-
|
|
825
|
+
try {
|
|
826
|
+
const dir = fileURLToPath2(new URL(rel, import.meta.url));
|
|
827
|
+
if (existsSync2(join(dir, "index.html"))) return dir;
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
775
830
|
}
|
|
776
831
|
return void 0;
|
|
777
832
|
}
|
|
@@ -813,7 +868,7 @@ function parseArgs(argv) {
|
|
|
813
868
|
}
|
|
814
869
|
|
|
815
870
|
// src/server.ts
|
|
816
|
-
import { createServer } from "http";
|
|
871
|
+
import { createServer } from "node:http";
|
|
817
872
|
import { WebSocketServer } from "ws";
|
|
818
873
|
|
|
819
874
|
// src/rpc/errors.ts
|
|
@@ -831,16 +886,16 @@ var RpcError = class extends Error {
|
|
|
831
886
|
};
|
|
832
887
|
|
|
833
888
|
// src/rpc/handlers.ts
|
|
834
|
-
import { StringDecoder as StringDecoder3 } from "string_decoder";
|
|
835
|
-
import { timingSafeEqual } from "crypto";
|
|
836
|
-
import { platform } from "os";
|
|
837
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
889
|
+
import { StringDecoder as StringDecoder3 } from "node:string_decoder";
|
|
890
|
+
import { timingSafeEqual } from "node:crypto";
|
|
891
|
+
import { platform } from "node:os";
|
|
892
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
838
893
|
|
|
839
894
|
// src/version.ts
|
|
840
|
-
var BRIDGE_VERSION = "1.0.
|
|
895
|
+
var BRIDGE_VERSION = "1.0.3";
|
|
841
896
|
|
|
842
897
|
// src/ssh/logged.ts
|
|
843
|
-
import { randomUUID } from "crypto";
|
|
898
|
+
import { randomUUID } from "node:crypto";
|
|
844
899
|
var MAX_LOG_OUTPUT = 8 * 1024;
|
|
845
900
|
var targetOf = (t) => `${t.username}@${t.host}:${t.port}`;
|
|
846
901
|
function summarize(stdout, stderr) {
|
|
@@ -945,16 +1000,16 @@ var LoggedSsh = class {
|
|
|
945
1000
|
};
|
|
946
1001
|
|
|
947
1002
|
// src/devices/scan.ts
|
|
948
|
-
import { createSocket } from "dgram";
|
|
949
|
-
import { request } from "http";
|
|
950
|
-
import { networkInterfaces } from "os";
|
|
1003
|
+
import { createSocket } from "node:dgram";
|
|
1004
|
+
import { request } from "node:http";
|
|
1005
|
+
import { networkInterfaces } from "node:os";
|
|
951
1006
|
|
|
952
1007
|
// src/devices/ports.ts
|
|
953
|
-
import { connect as connect2 } from "net";
|
|
1008
|
+
import { connect as connect2 } from "node:net";
|
|
954
1009
|
|
|
955
1010
|
// src/devices/webos.ts
|
|
956
1011
|
import WebSocket from "ws";
|
|
957
|
-
import { connect } from "net";
|
|
1012
|
+
import { connect } from "node:net";
|
|
958
1013
|
var PROBE_TIMEOUT_MS = 1500;
|
|
959
1014
|
function probeSsap(host, port, secure, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
960
1015
|
return new Promise((resolve2) => {
|
|
@@ -1218,7 +1273,7 @@ async function scanNetwork(opts = {}) {
|
|
|
1218
1273
|
}
|
|
1219
1274
|
|
|
1220
1275
|
// src/ssh/luna.ts
|
|
1221
|
-
import { StringDecoder } from "string_decoder";
|
|
1276
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1222
1277
|
function shellQuote(value) {
|
|
1223
1278
|
return value.split("'").map((s) => `'${s}'`).join("\\'");
|
|
1224
1279
|
}
|
|
@@ -1339,7 +1394,7 @@ async function lunaSubscribe(pool, device, uri, params, onMessage, opts = {}) {
|
|
|
1339
1394
|
}
|
|
1340
1395
|
|
|
1341
1396
|
// src/devices/keys.ts
|
|
1342
|
-
import { createHash } from "crypto";
|
|
1397
|
+
import { createHash } from "node:crypto";
|
|
1343
1398
|
import ssh2 from "ssh2";
|
|
1344
1399
|
var { utils } = ssh2;
|
|
1345
1400
|
function verifyKey(privateKey, passphrase) {
|
|
@@ -1360,7 +1415,7 @@ function verifyKey(privateKey, passphrase) {
|
|
|
1360
1415
|
}
|
|
1361
1416
|
|
|
1362
1417
|
// src/devices/keyserver.ts
|
|
1363
|
-
import { request as request2 } from "http";
|
|
1418
|
+
import { request as request2 } from "node:http";
|
|
1364
1419
|
var MAX_KEY_RESPONSE = 65536;
|
|
1365
1420
|
var TIMEOUT_MS = 1e4;
|
|
1366
1421
|
var MAX_REDIRECTS = 5;
|
|
@@ -1440,7 +1495,7 @@ async function fetchKey(host, port = KEY_SERVER_PORT) {
|
|
|
1440
1495
|
}
|
|
1441
1496
|
|
|
1442
1497
|
// src/devices/info.ts
|
|
1443
|
-
import { createHash as createHash2 } from "crypto";
|
|
1498
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1444
1499
|
import ssh22 from "ssh2";
|
|
1445
1500
|
|
|
1446
1501
|
// src/ssh/transfer.ts
|
|
@@ -1723,8 +1778,8 @@ async function renewDevMode(pool, device) {
|
|
|
1723
1778
|
}
|
|
1724
1779
|
|
|
1725
1780
|
// src/apps/apps.ts
|
|
1726
|
-
import { createHash as createHash3 } from "crypto";
|
|
1727
|
-
import { posix } from "path";
|
|
1781
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1782
|
+
import { posix } from "node:path";
|
|
1728
1783
|
|
|
1729
1784
|
// src/ssh/serve.ts
|
|
1730
1785
|
var HEADER_LIMIT = 16 * 1024;
|
|
@@ -1813,7 +1868,7 @@ Connection: close\r
|
|
|
1813
1868
|
}
|
|
1814
1869
|
|
|
1815
1870
|
// src/apps/ipk.ts
|
|
1816
|
-
import { gunzipSync } from "zlib";
|
|
1871
|
+
import { gunzipSync } from "node:zlib";
|
|
1817
1872
|
function readIpkControl(ipk) {
|
|
1818
1873
|
try {
|
|
1819
1874
|
const control = arMember(ipk, /^control\.tar\.gz\/?$/);
|
|
@@ -2119,7 +2174,7 @@ async function installDownloadable(pool, device, pkg, download, progress) {
|
|
|
2119
2174
|
}
|
|
2120
2175
|
|
|
2121
2176
|
// src/files/files.ts
|
|
2122
|
-
import { posix as posix2 } from "path";
|
|
2177
|
+
import { posix as posix2 } from "node:path";
|
|
2123
2178
|
var S_IFMT = 61440;
|
|
2124
2179
|
var TYPES = {
|
|
2125
2180
|
16384: "d",
|
|
@@ -2358,9 +2413,9 @@ async function removePath(pool, device, path) {
|
|
|
2358
2413
|
}
|
|
2359
2414
|
|
|
2360
2415
|
// src/debug/debug.ts
|
|
2361
|
-
import { posix as posix3 } from "path";
|
|
2362
|
-
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
2363
|
-
import { createGunzip } from "zlib";
|
|
2416
|
+
import { posix as posix3 } from "node:path";
|
|
2417
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
2418
|
+
import { createGunzip } from "node:zlib";
|
|
2364
2419
|
var requireRoot = (device, what) => {
|
|
2365
2420
|
if (device.username !== "root") {
|
|
2366
2421
|
throw new RpcError(DeviceErrorCodes.WrongLogin, `${what} needs a rooted TV (logged in as root).`);
|
|
@@ -2603,7 +2658,7 @@ var handlers = {
|
|
|
2603
2658
|
);
|
|
2604
2659
|
}
|
|
2605
2660
|
session.authed = true;
|
|
2606
|
-
return { protocolVersion: PROTOCOL_VERSION, bridgeVersion: BRIDGE_VERSION, platform: platform() };
|
|
2661
|
+
return { protocolVersion: PROTOCOL_VERSION, bridgeVersion: BRIDGE_VERSION, platform: platform(), distribution: distribution() };
|
|
2607
2662
|
},
|
|
2608
2663
|
"system.ping": () => ({ now: Date.now() }),
|
|
2609
2664
|
"device.checkConnection": ({ host }) => checkConnection(host),
|
|
@@ -2869,8 +2924,8 @@ async function dispatch(raw, session, ctx) {
|
|
|
2869
2924
|
}
|
|
2870
2925
|
|
|
2871
2926
|
// src/http/static.ts
|
|
2872
|
-
import { createReadStream, statSync } from "fs";
|
|
2873
|
-
import { extname, join as join2, normalize, resolve, sep } from "path";
|
|
2927
|
+
import { createReadStream, statSync } from "node:fs";
|
|
2928
|
+
import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "node:path";
|
|
2874
2929
|
var MIME = {
|
|
2875
2930
|
".html": "text/html; charset=utf-8",
|
|
2876
2931
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -2898,13 +2953,29 @@ var statOf = (file) => {
|
|
|
2898
2953
|
return null;
|
|
2899
2954
|
}
|
|
2900
2955
|
};
|
|
2956
|
+
var headersFor = (name, size) => ({
|
|
2957
|
+
"content-type": MIME[extname(name)] ?? "application/octet-stream",
|
|
2958
|
+
"content-length": size,
|
|
2959
|
+
"cache-control": name.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable",
|
|
2960
|
+
...SECURITY_HEADERS
|
|
2961
|
+
});
|
|
2962
|
+
function serveFiles(files, urlPath, req, res) {
|
|
2963
|
+
let name = posix4.normalize(urlPath).replace(/^\/+/, "");
|
|
2964
|
+
if (!files.has(name)) name = "index.html";
|
|
2965
|
+
const body = files.get(name);
|
|
2966
|
+
if (!body) {
|
|
2967
|
+
res.writeHead(404, { "content-type": "text/plain", ...SECURITY_HEADERS }).end("Web UI missing from this build.");
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
res.writeHead(200, headersFor(name, body.byteLength));
|
|
2971
|
+
res.end(req.method === "HEAD" ? void 0 : body);
|
|
2972
|
+
}
|
|
2901
2973
|
function serveStatic(root, req, res) {
|
|
2902
2974
|
try {
|
|
2903
2975
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2904
2976
|
res.writeHead(405, { allow: "GET, HEAD", ...SECURITY_HEADERS }).end();
|
|
2905
2977
|
return;
|
|
2906
2978
|
}
|
|
2907
|
-
const base = resolve(root);
|
|
2908
2979
|
let urlPath;
|
|
2909
2980
|
try {
|
|
2910
2981
|
urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
|
|
@@ -2916,6 +2987,11 @@ function serveStatic(root, req, res) {
|
|
|
2916
2987
|
res.writeHead(400, SECURITY_HEADERS).end();
|
|
2917
2988
|
return;
|
|
2918
2989
|
}
|
|
2990
|
+
if (typeof root !== "string") {
|
|
2991
|
+
serveFiles(root, urlPath, req, res);
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const base = resolve(root);
|
|
2919
2995
|
let file = normalize(join2(base, urlPath));
|
|
2920
2996
|
if (!file.startsWith(base + sep) && file !== base) {
|
|
2921
2997
|
res.writeHead(403, SECURITY_HEADERS).end();
|
|
@@ -2930,12 +3006,7 @@ function serveStatic(root, req, res) {
|
|
|
2930
3006
|
res.writeHead(404, { "content-type": "text/plain", ...SECURITY_HEADERS }).end("Web UI not built. Run `pnpm build`.");
|
|
2931
3007
|
return;
|
|
2932
3008
|
}
|
|
2933
|
-
res.writeHead(200,
|
|
2934
|
-
"content-type": MIME[extname(file)] ?? "application/octet-stream",
|
|
2935
|
-
"content-length": st.size,
|
|
2936
|
-
"cache-control": file.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable",
|
|
2937
|
-
...SECURITY_HEADERS
|
|
2938
|
-
});
|
|
3009
|
+
res.writeHead(200, headersFor(file, st.size));
|
|
2939
3010
|
if (req.method === "HEAD") {
|
|
2940
3011
|
res.end();
|
|
2941
3012
|
return;
|
|
@@ -2948,7 +3019,7 @@ function serveStatic(root, req, res) {
|
|
|
2948
3019
|
}
|
|
2949
3020
|
|
|
2950
3021
|
// src/ssh/pool.ts
|
|
2951
|
-
import { createHash as createHash4 } from "crypto";
|
|
3022
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2952
3023
|
import ssh23 from "ssh2";
|
|
2953
3024
|
var { Client } = ssh23;
|
|
2954
3025
|
var IDLE_MS = 12e4;
|
|
@@ -3276,7 +3347,7 @@ var SshPool = class {
|
|
|
3276
3347
|
};
|
|
3277
3348
|
|
|
3278
3349
|
// src/rpc/uploads.ts
|
|
3279
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
3350
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3280
3351
|
var UploadBudget = class {
|
|
3281
3352
|
constructor(limit = MAX_UPLOAD_BYTES) {
|
|
3282
3353
|
this.limit = limit;
|
|
@@ -3379,13 +3450,13 @@ var UploadStore = class {
|
|
|
3379
3450
|
};
|
|
3380
3451
|
|
|
3381
3452
|
// src/repo/repo.ts
|
|
3382
|
-
import { createHash as createHash5 } from "crypto";
|
|
3383
|
-
import { lookup } from "dns/promises";
|
|
3384
|
-
import { lookup as dnsLookup } from "dns";
|
|
3385
|
-
import { request as httpRequest } from "http";
|
|
3386
|
-
import { request as httpsRequest } from "https";
|
|
3387
|
-
import { BlockList, isIP } from "net";
|
|
3388
|
-
import { Readable } from "stream";
|
|
3453
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3454
|
+
import { lookup } from "node:dns/promises";
|
|
3455
|
+
import { lookup as dnsLookup } from "node:dns";
|
|
3456
|
+
import { request as httpRequest } from "node:http";
|
|
3457
|
+
import { request as httpsRequest } from "node:https";
|
|
3458
|
+
import { BlockList, isIP } from "node:net";
|
|
3459
|
+
import { Readable } from "node:stream";
|
|
3389
3460
|
var untraced = (_t, _c, fn) => fn();
|
|
3390
3461
|
var CACHE_MS = 5 * 6e4;
|
|
3391
3462
|
var JSON_MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -3944,7 +4015,7 @@ function lookup2(entries, tag, variant) {
|
|
|
3944
4015
|
}
|
|
3945
4016
|
|
|
3946
4017
|
// src/shell/shells.ts
|
|
3947
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
4018
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3948
4019
|
var MAX_SHELLS = 8;
|
|
3949
4020
|
var FLUSH_MS2 = 8;
|
|
3950
4021
|
var FLUSH_BYTES = 64 * 1024;
|
|
@@ -4224,6 +4295,20 @@ function startServer(config, pool = new SshPool(), repo = new RepoClient(config.
|
|
|
4224
4295
|
});
|
|
4225
4296
|
}
|
|
4226
4297
|
|
|
4298
|
+
// src/open.ts
|
|
4299
|
+
import { spawn } from "node:child_process";
|
|
4300
|
+
function openInBrowser(url2) {
|
|
4301
|
+
if (!/^https:\/\/[a-z0-9.-]+\/?$/i.test(url2)) return;
|
|
4302
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url2]] : ["xdg-open", [url2]];
|
|
4303
|
+
try {
|
|
4304
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
4305
|
+
child.on("error", () => {
|
|
4306
|
+
});
|
|
4307
|
+
child.unref();
|
|
4308
|
+
} catch {
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4227
4312
|
// src/cli.ts
|
|
4228
4313
|
var HELP = `lg-dev-manager-bridge ${BRIDGE_VERSION}
|
|
4229
4314
|
|
|
@@ -4234,7 +4319,9 @@ Usage: lg-dev-manager-bridge [options]
|
|
|
4234
4319
|
--web-root <dir> Serve the web UI from this folder instead of the bundled one (env LGDM_WEB_ROOT)
|
|
4235
4320
|
--no-ui Don't serve the local page; use https://lg.scoty.uk only
|
|
4236
4321
|
--reset-token Generate a new pairing token (un-pairs existing browsers)
|
|
4322
|
+
--no-open The app only: don't open ${PUBLIC_WEB_ORIGIN} in the browser on start
|
|
4237
4323
|
--version Print the version
|
|
4324
|
+
--license Print the licenses of this program and what it includes
|
|
4238
4325
|
--help Show this help
|
|
4239
4326
|
`;
|
|
4240
4327
|
process.on("uncaughtException", (e) => console.error(`[bridge] unexpected error: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -4250,6 +4337,10 @@ async function main() {
|
|
|
4250
4337
|
`);
|
|
4251
4338
|
return;
|
|
4252
4339
|
}
|
|
4340
|
+
if (argv.includes("--license")) {
|
|
4341
|
+
process.stdout.write(licenseText());
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4253
4344
|
const config = parseArgs(argv);
|
|
4254
4345
|
await startServer(config);
|
|
4255
4346
|
const shown = config.host;
|
|
@@ -4260,8 +4351,19 @@ async function main() {
|
|
|
4260
4351
|
console.log(` Pairing token ${config.token}`);
|
|
4261
4352
|
console.log(` Allowed origins ${config.allowedOrigins.join(", ")}
|
|
4262
4353
|
`);
|
|
4354
|
+
if (isStandalone()) {
|
|
4355
|
+
console.log(" Keep this window open while you use the app. Close it (or press Ctrl+C) to stop the bridge.\n");
|
|
4356
|
+
if (!argv.includes("--no-open")) openInBrowser(PUBLIC_WEB_ORIGIN);
|
|
4357
|
+
}
|
|
4263
4358
|
}
|
|
4264
4359
|
main().catch((e) => {
|
|
4265
|
-
console.error(
|
|
4360
|
+
console.error(
|
|
4361
|
+
e.code === "EADDRINUSE" ? `[bridge] failed to start: port ${/:(\d+)$/.exec(e.message)?.[1] ?? ""} is already in use. Is the bridge already running in another window? Close that one, or start this one with --port <another port>.` : `[bridge] failed to start: ${e.message}`
|
|
4362
|
+
);
|
|
4363
|
+
if (isStandalone() && process.stdin.isTTY) {
|
|
4364
|
+
console.error("\nPress Enter to close this window.");
|
|
4365
|
+
process.stdin.once("data", () => process.exit(1));
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4266
4368
|
process.exit(1);
|
|
4267
4369
|
});
|
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.3",
|
|
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)",
|
|
@@ -45,12 +45,14 @@
|
|
|
45
45
|
"@types/node": "^22.10.0",
|
|
46
46
|
"@types/ssh2": "^1.15.6",
|
|
47
47
|
"@types/ws": "^8.5.0",
|
|
48
|
+
"esbuild": "^0.27.7",
|
|
49
|
+
"postject": "1.0.0-alpha.6",
|
|
48
50
|
"tsup": "^8.5.0",
|
|
49
51
|
"tsx": "^4.20.0",
|
|
50
52
|
"typescript": "^5.9.0",
|
|
51
53
|
"vitest": "^4.1.11",
|
|
52
|
-
"@lgdm/mock-tv": "1.0.
|
|
53
|
-
"@lgdm/protocol": "1.0.
|
|
54
|
+
"@lgdm/mock-tv": "1.0.3",
|
|
55
|
+
"@lgdm/protocol": "1.0.3"
|
|
54
56
|
},
|
|
55
57
|
"scripts": {
|
|
56
58
|
"dev": "tsx watch src/cli.ts --dev",
|