@firstpick/pi-package-remote-webui 0.1.0 → 0.1.1
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 +12 -10
- package/index.ts +45 -3
- package/lib/remote-core.mjs +52 -7
- package/package.json +1 -1
- package/tests/remote-args.test.mjs +34 -10
- package/tests/remote-webui-control.test.mjs +71 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Mobile connection helper for [Pi coding agent](https://www.npmjs.com/package/@ea
|
|
|
4
4
|
|
|
5
5
|
This package adds a `/remote` slash command that reuses the existing `@firstpick/pi-package-webui` server/UI, opens it to a trusted local network, and shows a QR code in Pi so a phone can connect quickly.
|
|
6
6
|
|
|
7
|
-
> **Security:** Pi Web UI can control the Web UI/Pi session.
|
|
7
|
+
> **Security:** Pi Web UI can control the Web UI/Pi session. `/remote` asks whether to activate Remote PIN authentication before showing the QR code. Use `/remote` only on trusted local networks and close LAN access when done.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -23,9 +23,10 @@ Restart Pi after installation so the `/remote` command is loaded. The QR rendere
|
|
|
23
23
|
Default behavior:
|
|
24
24
|
|
|
25
25
|
1. Reuse a running Pi Web UI on `127.0.0.1:31415`, or start one for the current working directory.
|
|
26
|
-
2.
|
|
27
|
-
3.
|
|
28
|
-
4.
|
|
26
|
+
2. Ask whether to activate Remote PIN auth when it is currently off.
|
|
27
|
+
3. Open the Web UI listener to the local network through the existing Web UI `/api/network/open` endpoint.
|
|
28
|
+
4. Show a terminal QR code, the LAN URL, and the current Remote PIN auth state.
|
|
29
|
+
5. Scan the QR code from your phone and use the normal Pi Web UI. If Remote PIN auth is enabled and the local server reports the PIN, the QR code opens an auth link that signs in automatically; the displayed PIN remains a manual fallback.
|
|
29
30
|
|
|
30
31
|
## Commands
|
|
31
32
|
|
|
@@ -41,24 +42,25 @@ Default behavior:
|
|
|
41
42
|
|
|
42
43
|
| Command | Behavior |
|
|
43
44
|
|---|---|
|
|
44
|
-
| `/remote` | Start/reuse Web UI, confirm, open LAN access, and show QR plus
|
|
45
|
+
| `/remote` | Start/reuse Web UI, confirm LAN access, ask whether to activate Remote PIN auth, open LAN access, and show QR plus auth state. |
|
|
45
46
|
| `/remote status` | Show Web UI online/network state, LAN URLs, and auth state. |
|
|
46
47
|
| `/remote refresh` | Re-read current LAN URL/auth state and redraw the QR widget. |
|
|
47
48
|
| `/remote close` | Close Web UI LAN exposure and clear the QR widget. |
|
|
48
49
|
| `/remote --port 31500` | Use another Web UI port. |
|
|
49
50
|
| `/remote --name mobile` | Name the initial Web UI tab when this package starts the server. |
|
|
50
|
-
| `/remote --yes` | Skip
|
|
51
|
+
| `/remote --yes` | Skip prompts and activate Remote PIN auth automatically before opening LAN access. |
|
|
51
52
|
|
|
52
53
|
## Remote PIN auth
|
|
53
54
|
|
|
54
|
-
`/remote`
|
|
55
|
+
`/remote` checks the Web UI server's auth state before opening LAN access:
|
|
55
56
|
|
|
56
|
-
-
|
|
57
|
+
- If Remote PIN auth is off, `/remote` asks whether to activate it.
|
|
58
|
+
- `/remote --yes` treats the auth prompt as accepted and activates it automatically.
|
|
57
59
|
- Enabling it generates a random 4-digit PIN.
|
|
58
|
-
- Non-local browser clients must
|
|
60
|
+
- Non-local browser clients must authenticate before reaching Web UI.
|
|
59
61
|
- Localhost clients can always use the UI and toggle the setting.
|
|
60
62
|
|
|
61
|
-
The `/remote` QR widget shows `Remote PIN auth: off` or `Remote PIN auth: on · PIN 1234` when the Web UI server reports it. You can also start Web UI with auth already enabled by using `pi-webui --remote-auth` or `/webui-start --remote-auth` from `@firstpick/pi-package-webui`.
|
|
63
|
+
The `/remote` QR widget shows `Remote PIN auth: off` or `Remote PIN auth: on · PIN 1234` when the Web UI server reports it. When a PIN is available, the QR code targets `/remote-auth#pin=1234` so the phone can authenticate automatically without typing the PIN; the fragment is scrubbed by the auth page before it posts to the server. You can also start Web UI with auth already enabled by using `pi-webui --remote-auth` or `/webui-start --remote-auth` from `@firstpick/pi-package-webui`.
|
|
62
64
|
|
|
63
65
|
## Caveat
|
|
64
66
|
|
package/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
generateQrLines,
|
|
15
15
|
openRemoteWebui,
|
|
16
16
|
parseRemoteArgs,
|
|
17
|
+
remoteAuthQrUrl,
|
|
17
18
|
requiresOpenConfirmation,
|
|
18
19
|
usage,
|
|
19
20
|
} from "./lib/remote-core.mjs";
|
|
@@ -25,13 +26,21 @@ const LOCAL_HOST = "127.0.0.1";
|
|
|
25
26
|
|
|
26
27
|
const OPEN_WARNING = [
|
|
27
28
|
"Pi Web UI can control Pi/WebUI and run allowed tools from connected browsers.",
|
|
28
|
-
"
|
|
29
|
+
"You will be asked whether to activate Remote PIN auth before the QR code is shown.",
|
|
29
30
|
"",
|
|
30
31
|
"Only open this on a trusted local network.",
|
|
31
32
|
"",
|
|
32
33
|
"Open to local network?",
|
|
33
34
|
].join("\n");
|
|
34
35
|
|
|
36
|
+
const AUTH_WARNING = [
|
|
37
|
+
"Remote PIN auth is currently off.",
|
|
38
|
+
"",
|
|
39
|
+
"Activate it now? /remote will embed the generated PIN in the QR code so your phone can sign in without typing it.",
|
|
40
|
+
"",
|
|
41
|
+
"Choose No only on a trusted LAN where anyone with the URL may control Pi/WebUI.",
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
35
44
|
type WebuiChild = ChildProcessByStdio<null, Readable, Readable>;
|
|
36
45
|
|
|
37
46
|
type RemoteOptions = {
|
|
@@ -41,6 +50,11 @@ type RemoteOptions = {
|
|
|
41
50
|
yes: boolean;
|
|
42
51
|
};
|
|
43
52
|
|
|
53
|
+
type RemoteNetworkStatus = {
|
|
54
|
+
auth?: { enabled?: boolean; pin?: string };
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
|
|
44
58
|
function resolveExistingPath(candidate: string | undefined): string | undefined {
|
|
45
59
|
if (!candidate) return undefined;
|
|
46
60
|
return existsSync(candidate) ? candidate : undefined;
|
|
@@ -174,8 +188,9 @@ function setFullRemoteWidget(ctx: ExtensionCommandContext, lines: string[]): voi
|
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
async function renderRemoteWidget(ctx: ExtensionCommandContext, result: { url: string; network?: unknown; started?: boolean }): Promise<void> {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
191
|
+
const qrUrl = remoteAuthQrUrl(result.url, result.network || {});
|
|
192
|
+
const qrLines = await generateQrLines(qrUrl);
|
|
193
|
+
const lines = buildRemoteWidgetLines({ url: result.url, qrUrl, qrLines, network: result.network, started: result.started });
|
|
179
194
|
setFullRemoteWidget(ctx, lines);
|
|
180
195
|
setRemoteStatus(ctx, `remote ${result.url}`);
|
|
181
196
|
}
|
|
@@ -186,6 +201,32 @@ async function confirmRemoteOpen(options: RemoteOptions, ctx: ExtensionCommandCo
|
|
|
186
201
|
return await ctx.ui.confirm("Open Pi Web UI to LAN?", OPEN_WARNING);
|
|
187
202
|
}
|
|
188
203
|
|
|
204
|
+
function remoteAuthEnabled(network: unknown): boolean {
|
|
205
|
+
return (network as RemoteNetworkStatus | undefined)?.auth?.enabled === true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function networkFromAuthUpdate(previous: unknown, data: unknown): unknown {
|
|
209
|
+
const update = data as { network?: unknown; auth?: unknown } | undefined;
|
|
210
|
+
if (update?.network) return update.network;
|
|
211
|
+
if (update?.auth && previous && typeof previous === "object") return { ...(previous as RemoteNetworkStatus), auth: update.auth };
|
|
212
|
+
return previous;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function maybeActivateRemoteAuth(options: RemoteOptions, ctx: ExtensionCommandContext, controller: RemoteWebuiController, network: unknown): Promise<unknown> {
|
|
216
|
+
if (remoteAuthEnabled(network)) return network;
|
|
217
|
+
|
|
218
|
+
let activate = options.yes === true;
|
|
219
|
+
if (!activate) {
|
|
220
|
+
if (!ctx.hasUI) return network;
|
|
221
|
+
activate = await ctx.ui.confirm("Activate Remote PIN auth?", AUTH_WARNING);
|
|
222
|
+
}
|
|
223
|
+
if (!activate) return network;
|
|
224
|
+
|
|
225
|
+
setRemoteStatus(ctx, "enabling remote PIN auth…");
|
|
226
|
+
const data = await controller.setRemoteAuth(options.port, true);
|
|
227
|
+
return networkFromAuthUpdate(network, data);
|
|
228
|
+
}
|
|
229
|
+
|
|
189
230
|
async function handleStatus(options: RemoteOptions, ctx: ExtensionCommandContext, controller: RemoteWebuiController): Promise<void> {
|
|
190
231
|
setRemoteStatus(ctx, "checking remote webui…");
|
|
191
232
|
try {
|
|
@@ -243,6 +284,7 @@ async function handleOpen(options: RemoteOptions, ctx: ExtensionCommandContext,
|
|
|
243
284
|
const result = await openRemoteWebui(options, {
|
|
244
285
|
controller,
|
|
245
286
|
startWebui: async (startOptions: RemoteOptions) => spawnWebui(startOptions, ctx),
|
|
287
|
+
prepareNetwork: async (network: unknown) => maybeActivateRemoteAuth(options, ctx, controller, network),
|
|
246
288
|
});
|
|
247
289
|
await renderRemoteWidget(ctx, result);
|
|
248
290
|
ctx.ui.notify(`Pi Remote WebUI ready:\n${result.url}\n\nScan the QR code above from your phone.`, "info");
|
package/lib/remote-core.mjs
CHANGED
|
@@ -201,6 +201,16 @@ export class RemoteWebuiController {
|
|
|
201
201
|
throw new Error(result.body?.error || "Failed to close Pi Web UI network access");
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
async setRemoteAuth(port, enabled, timeoutMs = 1_500) {
|
|
205
|
+
const result = await fetchJsonWithTimeout(endpointUrl(port, "/api/remote-auth/settings"), {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "content-type": "application/json" },
|
|
208
|
+
body: JSON.stringify({ enabled: enabled === true }),
|
|
209
|
+
}, timeoutMs, this.fetchImpl);
|
|
210
|
+
if (result.ok && result.body?.ok === true) return result.body.data;
|
|
211
|
+
throw new Error(result.body?.error || "Failed to update Pi Web UI Remote PIN auth");
|
|
212
|
+
}
|
|
213
|
+
|
|
204
214
|
async waitForNetworkOpen(port, { timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS, pollMs = DEFAULT_POLL_MS } = {}) {
|
|
205
215
|
const deadline = Date.now() + timeoutMs;
|
|
206
216
|
let last;
|
|
@@ -235,7 +245,31 @@ export function selectLanUrl(network) {
|
|
|
235
245
|
return urls.find((url) => typeof url === "string" && /^https?:\/\//i.test(url)) || undefined;
|
|
236
246
|
}
|
|
237
247
|
|
|
238
|
-
|
|
248
|
+
function safeQrReturnPath(value = "/") {
|
|
249
|
+
const text = String(value || "/").trim();
|
|
250
|
+
if (!text.startsWith("/") || text.startsWith("//")) return "/";
|
|
251
|
+
return text;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function remoteAuthQrUrl(url, network = {}) {
|
|
255
|
+
if (typeof url !== "string" || !url) return url;
|
|
256
|
+
const pin = String(network?.auth?.pin || "").trim();
|
|
257
|
+
if (!network?.auth?.enabled || !/^\d{4}$/.test(pin)) return url;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const parsed = new URL(url);
|
|
261
|
+
const returnPath = safeQrReturnPath(`${parsed.pathname || "/"}${parsed.search || ""}`);
|
|
262
|
+
parsed.pathname = "/remote-auth";
|
|
263
|
+
parsed.search = "";
|
|
264
|
+
parsed.searchParams.set("return", returnPath);
|
|
265
|
+
parsed.hash = new URLSearchParams({ pin }).toString();
|
|
266
|
+
return parsed.toString();
|
|
267
|
+
} catch {
|
|
268
|
+
return url;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function openRemoteWebui(options, { controller, startWebui, prepareNetwork } = {}) {
|
|
239
273
|
if (!controller) throw new Error("RemoteWebuiController is required");
|
|
240
274
|
let health = await controller.probeHealth(options.port);
|
|
241
275
|
let started = false;
|
|
@@ -254,6 +288,10 @@ export async function openRemoteWebui(options, { controller, startWebui }) {
|
|
|
254
288
|
network = health.data?.network;
|
|
255
289
|
}
|
|
256
290
|
|
|
291
|
+
if (typeof prepareNetwork === "function") {
|
|
292
|
+
network = (await prepareNetwork(network, { health, started })) || network;
|
|
293
|
+
}
|
|
294
|
+
|
|
257
295
|
if (!network?.open || network?.closing) {
|
|
258
296
|
await controller.openNetwork(options.port);
|
|
259
297
|
network = await controller.waitForNetworkOpen(options.port);
|
|
@@ -299,25 +337,32 @@ export function formatStatus(status) {
|
|
|
299
337
|
`Bind: ${network.host || "unknown"}:${network.port || "?"}`,
|
|
300
338
|
];
|
|
301
339
|
if (networkUrls.length) lines.push(`LAN URLs: ${networkUrls.join(", ")}`);
|
|
340
|
+
const auth = network.auth || {};
|
|
341
|
+
lines.push(auth.enabled ? `Remote PIN auth: on${auth.pin ? ` · PIN ${auth.pin}` : ""}` : "Remote PIN auth: off");
|
|
302
342
|
if (status.health?.data?.webuiVersion) lines.push(`Version: ${status.health.data.webuiVersion}`);
|
|
303
343
|
if (status.error) lines.push(`Warning: ${status.error}`);
|
|
304
344
|
return lines.join("\n");
|
|
305
345
|
}
|
|
306
346
|
|
|
307
|
-
export function buildRemoteWidgetLines({ url, qrLines = [], network = {}, started = false } = {}) {
|
|
347
|
+
export function buildRemoteWidgetLines({ url, qrUrl, qrLines = [], network = {}, started = false } = {}) {
|
|
308
348
|
const auth = network?.auth || {};
|
|
349
|
+
const displayUrl = url || selectLanUrl(network) || network.localUrl || "(no URL)";
|
|
350
|
+
const qrTarget = qrUrl || remoteAuthQrUrl(displayUrl, network);
|
|
351
|
+
const hasAutoAuthQr = auth.enabled && !!auth.pin && qrTarget !== displayUrl;
|
|
309
352
|
const authLine = auth.enabled ? `Remote PIN auth: on${auth.pin ? ` · PIN ${auth.pin}` : ""}` : "Remote PIN auth: off";
|
|
310
|
-
const warningLine =
|
|
311
|
-
? "Trusted LAN only.
|
|
312
|
-
:
|
|
353
|
+
const warningLine = hasAutoAuthQr
|
|
354
|
+
? "Trusted LAN only. The QR signs in with the embedded PIN; keep it private."
|
|
355
|
+
: auth.enabled
|
|
356
|
+
? "Trusted LAN only. Anyone with this URL and PIN can control Pi/WebUI."
|
|
357
|
+
: "Trusted LAN only. Remote PIN auth is off; anyone with this URL can control Pi/WebUI.";
|
|
313
358
|
const lines = [
|
|
314
359
|
"Pi Remote WebUI",
|
|
315
360
|
"",
|
|
316
|
-
"Scan with your phone:",
|
|
361
|
+
hasAutoAuthQr ? "Scan with your phone (auto-auth QR):" : "Scan with your phone:",
|
|
317
362
|
"",
|
|
318
363
|
...qrLines,
|
|
319
364
|
"",
|
|
320
|
-
|
|
365
|
+
displayUrl,
|
|
321
366
|
authLine,
|
|
322
367
|
"",
|
|
323
368
|
warningLine,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-remote-webui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Pi /remote command that opens the existing Pi Web UI to a trusted LAN and shows a QR code for mobile.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/Firstp1ck/npm-packages/tree/main/pi-package-remote-webui#readme",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_PORT,
|
|
5
6
|
buildRemoteWidgetLines,
|
|
6
7
|
formatStatus,
|
|
7
8
|
generateQrLines,
|
|
8
9
|
parseRemoteArgs,
|
|
10
|
+
remoteAuthQrUrl,
|
|
9
11
|
requiresOpenConfirmation,
|
|
10
12
|
selectLanUrl,
|
|
11
13
|
tokenizeArgs,
|
|
@@ -60,19 +62,33 @@ test("selectLanUrl chooses the first HTTP LAN URL", () => {
|
|
|
60
62
|
);
|
|
61
63
|
});
|
|
62
64
|
|
|
63
|
-
test("
|
|
64
|
-
assert.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
test("remoteAuthQrUrl embeds an available remote PIN in a URL fragment auth link", () => {
|
|
66
|
+
assert.equal(
|
|
67
|
+
remoteAuthQrUrl("http://192.168.1.20:31415/", { auth: { enabled: true, pin: "1234" } }),
|
|
68
|
+
"http://192.168.1.20:31415/remote-auth?return=%2F#pin=1234",
|
|
69
|
+
);
|
|
70
|
+
assert.equal(
|
|
71
|
+
remoteAuthQrUrl("http://192.168.1.20:31415/tree?tab=abc", { auth: { enabled: true, pin: "1234" } }),
|
|
72
|
+
"http://192.168.1.20:31415/remote-auth?return=%2Ftree%3Ftab%3Dabc#pin=1234",
|
|
73
|
+
);
|
|
74
|
+
assert.equal(
|
|
75
|
+
remoteAuthQrUrl("http://192.168.1.20:31415/", { auth: { enabled: true } }),
|
|
76
|
+
"http://192.168.1.20:31415/",
|
|
73
77
|
);
|
|
74
78
|
});
|
|
75
79
|
|
|
80
|
+
test("formatStatus renders offline, online, and auth states", () => {
|
|
81
|
+
assert.match(formatStatus({ online: false, url: "http://127.0.0.1:31415/", health: { error: "offline" } }), /Online:\s+no/);
|
|
82
|
+
const onlineStatus = formatStatus({
|
|
83
|
+
online: true,
|
|
84
|
+
url: "http://192.168.1.20:31415/",
|
|
85
|
+
health: { data: { webuiVersion: "0.3.8" } },
|
|
86
|
+
network: { open: true, host: "0.0.0.0", port: 31415, networkUrls: ["http://192.168.1.20:31415/"], auth: { enabled: true, pin: "1234" } },
|
|
87
|
+
});
|
|
88
|
+
assert.match(onlineStatus, /open to LAN/);
|
|
89
|
+
assert.match(onlineStatus, /Remote PIN auth: on · PIN 1234/);
|
|
90
|
+
});
|
|
91
|
+
|
|
76
92
|
test("buildRemoteWidgetLines includes QR, URL, auth state, and close instruction", () => {
|
|
77
93
|
const lines = buildRemoteWidgetLines({
|
|
78
94
|
url: "http://192.168.1.20:31415/",
|
|
@@ -83,6 +99,7 @@ test("buildRemoteWidgetLines includes QR, URL, auth state, and close instruction
|
|
|
83
99
|
assert(lines.includes("QR-A"));
|
|
84
100
|
assert(lines.includes("http://192.168.1.20:31415/"));
|
|
85
101
|
assert(lines.some((line) => line.includes("PIN 1234")));
|
|
102
|
+
assert(lines.some((line) => line.includes("QR signs in")));
|
|
86
103
|
assert(lines.some((line) => line.includes("/remote close")));
|
|
87
104
|
assert(lines.some((line) => line.includes("Started a Pi Web UI server")));
|
|
88
105
|
});
|
|
@@ -110,3 +127,10 @@ test("generateQrLines reports QR failures without duplicating the URL", async ()
|
|
|
110
127
|
});
|
|
111
128
|
assert.deepEqual(lines, ["[QR generation failed: boom]"]);
|
|
112
129
|
});
|
|
130
|
+
|
|
131
|
+
test("extension asks whether to activate Remote PIN auth while opening /remote", async () => {
|
|
132
|
+
const source = await readFile(new URL("../index.ts", import.meta.url), "utf8");
|
|
133
|
+
assert.match(source, /ctx\.ui\.confirm\("Activate Remote PIN auth\?", AUTH_WARNING\)/);
|
|
134
|
+
assert.match(source, /controller\.setRemoteAuth\(options\.port, true\)/);
|
|
135
|
+
assert.match(source, /prepareNetwork: async \(network: unknown\) => maybeActivateRemoteAuth/);
|
|
136
|
+
});
|
|
@@ -75,6 +75,77 @@ test("openRemoteWebui starts when offline, opens network, and returns a LAN URL"
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("openRemoteWebui can prepare network settings before opening LAN access", async () => {
|
|
79
|
+
const calls = [];
|
|
80
|
+
let networkOpen = false;
|
|
81
|
+
let authEnabled = false;
|
|
82
|
+
|
|
83
|
+
await withMockWebui((req, res) => {
|
|
84
|
+
calls.push(`${req.method} ${req.url}`);
|
|
85
|
+
if (req.url === "/api/health" && req.method === "GET") return sendJson(res, 200, { ok: true, webuiVersion: "0.3.8" });
|
|
86
|
+
if (req.url === "/api/network" && req.method === "GET") {
|
|
87
|
+
return sendJson(res, 200, {
|
|
88
|
+
ok: true,
|
|
89
|
+
data: {
|
|
90
|
+
open: networkOpen,
|
|
91
|
+
opening: false,
|
|
92
|
+
closing: false,
|
|
93
|
+
host: networkOpen ? "0.0.0.0" : "127.0.0.1",
|
|
94
|
+
port: 0,
|
|
95
|
+
localUrl: "http://127.0.0.1/",
|
|
96
|
+
networkUrls: networkOpen ? ["http://10.0.0.8:31415/"] : [],
|
|
97
|
+
auth: authEnabled ? { enabled: true, pin: "1234" } : { enabled: false },
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (req.url === "/api/remote-auth/settings" && req.method === "POST") {
|
|
102
|
+
authEnabled = true;
|
|
103
|
+
return sendJson(res, 200, {
|
|
104
|
+
ok: true,
|
|
105
|
+
data: {
|
|
106
|
+
network: {
|
|
107
|
+
open: networkOpen,
|
|
108
|
+
opening: false,
|
|
109
|
+
closing: false,
|
|
110
|
+
host: networkOpen ? "0.0.0.0" : "127.0.0.1",
|
|
111
|
+
port: 0,
|
|
112
|
+
localUrl: "http://127.0.0.1/",
|
|
113
|
+
networkUrls: networkOpen ? ["http://10.0.0.8:31415/"] : [],
|
|
114
|
+
auth: { enabled: true, pin: "1234" },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (req.url === "/api/network/open" && req.method === "POST") {
|
|
120
|
+
networkOpen = true;
|
|
121
|
+
return sendJson(res, 202, { ok: true, data: { opening: true } });
|
|
122
|
+
}
|
|
123
|
+
sendJson(res, 404, { ok: false });
|
|
124
|
+
}, async (port) => {
|
|
125
|
+
const controller = new RemoteWebuiController({ sleepImpl: () => Promise.resolve() });
|
|
126
|
+
const result = await openRemoteWebui({ port }, {
|
|
127
|
+
controller,
|
|
128
|
+
startWebui: async () => {},
|
|
129
|
+
prepareNetwork: async (network) => {
|
|
130
|
+
calls.push(`prepare auth=${network.auth.enabled}`);
|
|
131
|
+
const data = await controller.setRemoteAuth(port, true);
|
|
132
|
+
return data.network;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
assert.equal(result.url, "http://10.0.0.8:31415/");
|
|
137
|
+
assert.equal(result.network.auth.pin, "1234");
|
|
138
|
+
assert.deepEqual(calls, [
|
|
139
|
+
"GET /api/health",
|
|
140
|
+
"GET /api/network",
|
|
141
|
+
"prepare auth=false",
|
|
142
|
+
"POST /api/remote-auth/settings",
|
|
143
|
+
"POST /api/network/open",
|
|
144
|
+
"GET /api/network",
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
78
149
|
test("openRemoteWebui reuses an already open WebUI", async () => {
|
|
79
150
|
let startCalled = false;
|
|
80
151
|
|