@deepseek-ai/dsh-web-app 0.2.1-alpha.1 → 0.2.1-alpha.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/lib/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { t as parsePublicUrl } from "./public-url-D_dMK-yI.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { spawn } from "node:child_process";
4
+ import { isIP } from "node:net";
4
5
  import { dirname, join } from "node:path";
5
- import { networkInterfaces } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import { addHarnessSourceSection, auditStartupEntries } from "@deepseek-ai/dsh-app-boot";
9
9
  import * as FrontendStatic from "@deepseek-ai/dsh-host-frontend-static";
10
+ import { isLoopbackHost, normalizeBindAddress } from "@deepseek-ai/dsh-host-webserver";
10
11
  import { launchEnvironmentOf, launchedThroughSsh } from "@deepseek-ai/dsh-launch-environment";
11
12
  import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
12
13
  //#region lib/types/index.js
@@ -18,9 +19,9 @@ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
18
19
  * config), mounts the `frontend-static` fallback owner over it, registers the
19
20
  * harness-source and web-surface prompt sections, the bash-visible web runtime
20
21
  * variable, the process-token URL line, and the default-browser handoff. An
21
- * advertised `publicUrl` replaces the published root — the loopback URL
22
- * otherwise. App command-line values arrive through the `webStartup` service
23
- * expressions in the bundle patch.
22
+ * advertised `publicUrl` replaces the published bind-address URL. Non-loopback
23
+ * HTTP binds trigger an exposure warning. App command-line values arrive
24
+ * through the `webStartup` service expressions in the bundle patch.
24
25
  * @module @deepseek-ai/dsh-web-app
25
26
  */
26
27
  /** Stable Cordis plugin name. */
@@ -28,22 +29,16 @@ const name = "web-app";
28
29
  /** This dsh installation's root, from either this package's source or built entry. */
29
30
  const SOURCE_ROOT = fileURLToPath(new URL("../../../..", import.meta.url));
30
31
  const ANNOUNCED_ROOTS = /* @__PURE__ */ new WeakSet();
31
- /** Runtime service that releases Web rows after bind-dependent values resolve. */
32
- const WEB_RUNTIME_SERVICE = "webRuntime";
33
32
  /** Services required before the web runtime can mount. */
34
33
  const inject = ["webServer"];
35
34
  const Config = z.object({
36
35
  openBrowser: z.boolean().default(true),
37
36
  printUrl: z.boolean().default(true),
38
37
  surfaceContext: z.boolean().default(true),
39
- publicUrl: z.transform(z.string(), (value) => parsePublicUrl(value).href),
40
- trustedHosts: z.array(String).default([])
38
+ publicUrl: z.transform(z.string(), (value) => parsePublicUrl(value).href)
41
39
  });
42
40
  /** Environment variable naming the advertised URL of this Web GUI. */
43
41
  const DSH_WEB_URL = "DSH_WEB_URL";
44
- const LOOPBACK_HOST = "127.0.0.1";
45
- /** The webserver schema's all-interfaces bind literal. */
46
- const ALL_INTERFACES_HOST = "0.0.0.0";
47
42
  const BROWSER_OPENER_MODULE = import.meta.resolve("open");
48
43
  const BROWSER_OPENER_PROGRAM = `
49
44
  try {
@@ -74,36 +69,46 @@ try {
74
69
  }
75
70
  `;
76
71
  /**
77
- * Resolve one LAN-trust snapshot from the active server bind.
78
- *
79
- * Derived entries are port-less IP literals: DNS rebinding needs an
80
- * attacker-controlled name, while an IP-literal Host is safe on any port and
81
- * an OS-assigned port is unknowable before bind.
82
- * @param bindHost - the active webserver bind host.
83
- * @param extra - explicit `--trusted-host` values, in argument order.
84
- * @returns the LAN display addresses and invocation-derived fence authorities.
72
+ * Local root URL of the active Web server, using the browser-compatible form
73
+ * of its bind address and brackets for IPv6 literals. Non-loopback binds such
74
+ * as a container's Pod address advertise that address.
75
+ * @throws when the runtime resolves without a bound webServer, or when the bind
76
+ * address has no URL form (see {@link advertisedBindHost}).
85
77
  */
86
- function resolveLanTrust(bindHost, extra) {
87
- const lanAddresses = bindHost === ALL_INTERFACES_HOST ? Object.values(networkInterfaces()).flat().filter((iface) => iface !== void 0 && iface.family === "IPv4" && !iface.internal).map((iface) => iface.address) : [];
88
- return {
89
- lanAddresses,
90
- trustedHosts: [...lanAddresses, ...extra]
91
- };
92
- }
93
- /** Model-visible orientation and acceptance boundary for sessions created through `dsh web`. */
94
- function webSurfacePrompt(webUrl) {
95
- return `You are interacting with the user through the DeepSeek Harness Web GUI at ${webUrl}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while \`pnpm run dev:web\` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background job and verify its exact URL.`;
96
- }
97
- /** Resolve the canonical loopback URL from the active Web server. */
98
78
  function localWebUrl(ctx) {
99
- const port = ctx.get("webServer")?.port;
100
- if (port === void 0) throw new Error("web-app: webServer service missing while resolving Web runtime");
101
- return `http://${LOOPBACK_HOST}:${String(port)}`;
79
+ const webServer = ctx.get("webServer");
80
+ const port = webServer?.port;
81
+ if (webServer === void 0 || port === void 0) throw new Error("web-app: webServer service missing while resolving Web runtime");
82
+ const host = advertisedBindHost(webServer.host);
83
+ return `${webServer.protocol}//${isIP(host) === 6 ? `[${host}]` : host}:${String(port)}`;
84
+ }
85
+ /**
86
+ * Bind address in a URL. Loopback addresses use their canonical address text,
87
+ * so browsers treat them as local, trustworthy origins: a genuinely mapped
88
+ * literal such as `::ffff:127.0.0.1` reads as `127.0.0.1`, and a dotted-quad
89
+ * tail reads as the address it names, so `::0.0.0.1` reads as `::1` rather
90
+ * than as `0.0.0.1`. Interface zone IDs cannot appear in a URL; only a
91
+ * redundant loopback zone can be dropped.
92
+ * @param host - webserver bind address.
93
+ * @returns the zone-free URL host.
94
+ * @throws when a non-loopback zone requires an explicit public URL.
95
+ */
96
+ function advertisedBindHost(host) {
97
+ const zoneAt = host.indexOf("%");
98
+ const bare = zoneAt === -1 ? host : host.slice(0, zoneAt);
99
+ if (isLoopbackHost(bare)) return normalizeBindAddress(bare);
100
+ if (zoneAt === -1) return host;
101
+ throw new Error(`web-app: bind address ${JSON.stringify(host)} carries an interface zone id, which no URL can express; pass --public-url with the root browsers actually reach it through`);
102
102
  }
103
+ /** Resolve the advertised root: this plugin's canonical `publicUrl`, or the bind-address URL (loopback when bound to loopback). */
103
104
  function appRootUrl(ctx, publicUrl) {
104
105
  if (publicUrl !== void 0) return publicUrl;
105
106
  return localWebUrl(ctx);
106
107
  }
108
+ /** Model-visible orientation and acceptance boundary for sessions created through `dsh web`. */
109
+ function webSurfacePrompt(webUrl) {
110
+ return `You are interacting with the user through the DeepSeek Harness Web GUI at ${webUrl}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while \`pnpm run dev:web\` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background job and verify its exact URL.`;
111
+ }
107
112
  /**
108
113
  * Dist location is workspace knowledge of this bundle: anchored on the
109
114
  * frontend package manifest, not configured. Existence is a request-time
@@ -177,10 +182,11 @@ const internals = {
177
182
  * @param config - validated {@link Config}.
178
183
  */
179
184
  function apply(ctx, config) {
180
- const runtime = resolveLanTrust(ctx.webServer.host, config.trustedHosts);
181
185
  const publicUrl = config.publicUrl ?? void 0;
186
+ const boundHost = ctx.webServer.host;
187
+ if (publicUrl === void 0) advertisedBindHost(boundHost);
188
+ if (ctx.webServer.protocol === "http:" && !isLoopbackHost(boundHost)) console.warn(`dsh web: listening on ${boundHost} over plain HTTP; restrict this port to a trusted proxy or network`);
182
189
  const handoffBrowser = config.openBrowser && !launchedThroughSsh(launchEnvironmentOf(ctx));
183
- ctx.provide(WEB_RUNTIME_SERVICE, runtime);
184
190
  ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() });
185
191
  if (config.surfaceContext) {
186
192
  ctx.inject(["systemPrompt"], (promptCtx) => {
@@ -204,11 +210,8 @@ function apply(ctx, config) {
204
210
  if (ANNOUNCED_ROOTS.has(connectionCtx.root)) return;
205
211
  const webUrl = appRootUrl(connectionCtx, publicUrl);
206
212
  const authenticatedUrl = connectionCtx.connection.authenticatedUrl(webUrl);
207
- const lanCandidate = runtime.lanAddresses[0];
208
- const port = connectionCtx.webServer.port;
209
- const lanUrl = lanCandidate === void 0 ? void 0 : connectionCtx.connection.authenticatedUrl(`http://${lanCandidate}:${String(port)}`);
210
213
  ANNOUNCED_ROOTS.add(connectionCtx.root);
211
- if (config.printUrl) console.log(`dsh web: ${authenticatedUrl}${lanUrl === void 0 ? "" : ` (LAN: ${lanUrl})`}`);
214
+ if (config.printUrl) console.log(`dsh web: ${authenticatedUrl}`);
212
215
  if (handoffBrowser) {
213
216
  console.log("dsh web: opening the default browser; pass --no-open to disable");
214
217
  internals.openBrowser(authenticatedUrl).catch((error) => {
@@ -226,4 +229,4 @@ function apply(ctx, config) {
226
229
  });
227
230
  }
228
231
  //#endregion
229
- export { Config, apply, inject, internals, name, resolveLanTrust };
232
+ export { Config, apply, inject, internals, name };
package/lib/startup.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { t as parsePublicUrl } from "./public-url-D_dMK-yI.js";
2
+ import { isWildcardHost } from "@deepseek-ai/dsh-host-webserver";
2
3
  import { Command } from "commander";
3
4
  import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
4
5
  //#region lib/types/startup.js
5
6
  /**
6
7
  * The web app's command-line provider: it parses the `dsh --profile web` flag
7
- * family (`--host`, `--port`, `--public-url`, `--trusted-host`, `--no-open`)
8
+ * family (`--host`, `--port`, `--public-url`, `--trusted-host`, `--no-open`, TLS)
8
9
  * and its `--help` text, then provides the immutable values as
9
10
  * {@link WEB_STARTUP_SERVICE}. Ordinary rows inject that service before
10
11
  * reading it from lazy config.
@@ -21,28 +22,37 @@ const WEB_STARTUP_SERVICE = "webStartup";
21
22
  * @returns a fresh program, so one process can parse more than once (tests).
22
23
  */
23
24
  function webCommand() {
24
- return new Command().name("dsh --profile web").description("Serve the DeepSeek Harness browser UI.").helpOption("-h, --help", "show this help").option("--host <host>", "bind host").option("--no-open", "do not open the Web UI in the default browser").option("--port <port>", "listen port; pass 0 to let the OS pick a free one").option("--public-url <url>", "advertise this HTTP(S) root in the printed, opened, web-surface, and DSH_WEB_URL forms; grants no trust").option("--trusted-host <authority...>", "extra authority the /api browser-trust fence accepts (host or host:port; repeatable)").addHelpText("after", `
25
+ return new Command().name("dsh --profile web").description("Serve the DeepSeek Harness browser UI.").helpOption("-h, --help", "show this help").option("--host <host>", "bind address: one concrete IPv4 or IPv6 literal of a local interface; wildcard addresses are rejected").option("--no-open", "do not open the Web UI in the default browser").option("--port <port>", "listen port; pass 0 to let the OS pick a free one").option("--public-url <url>", "advertise this HTTP(S) root in the printed, opened, web-surface, and DSH_WEB_URL forms; grants no trust").option("--trusted-host <authority...>", "extra authority the /api browser-trust fence accepts (host or host:port; repeatable)").option("--tls-cert <file>", "PEM server certificate chain; requires --tls-key").option("--tls-key <file>", "unencrypted PEM private key; requires --tls-cert").addHelpText("after", `
25
26
  Examples:
26
27
  dsh --profile web serve on the composed host and port
27
28
  dsh --profile web --no-open serve without opening a browser
28
29
  dsh --profile web --port 8080 serve on another port
30
+ dsh --profile web --host 10.0.0.7 bind one local interface address
29
31
  dsh --profile web --public-url https://app.example/ui/ --trusted-host app.example
30
32
  advertise a prefix-stripping HTTPS proxy entry and admit its authority
33
+ dsh --profile web --tls-cert ./server-chain.pem --tls-key ./server-key.pem
34
+ serve HTTPS with an existing certificate and key
31
35
  `);
32
36
  }
33
37
  /**
34
38
  * Parse and provide the Web invocation as an ordinary Cordis service. The
35
- * command's action publishes the flags this invocation named; `--host 0.0.0.0`,
36
- * a non-numeric `--port`, or a malformed `--public-url` is a usage error, so on
37
- * rejection (and on `--help`) nothing is provided.
39
+ * command's action publishes the flags this invocation named. Invalid flags,
40
+ * including an unpaired TLS certificate or key, are usage errors; rejection
41
+ * and `--help` provide no service.
38
42
  * @param ctx - plugin context carrying the command line.
39
43
  */
40
44
  function apply(ctx) {
41
45
  const program = webCommand();
42
46
  program.action(() => {
43
47
  const options = program.opts();
44
- if (options.host === "0.0.0.0") program.error("error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead");
48
+ if (options.host !== void 0 && isWildcardHost(options.host)) program.error(`error: --host ${options.host} is an unspecified (wildcard) address, which is not supported: binding every interface would expose remote code execution to the network; bind one concrete IPv4 or IPv6 address instead`);
45
49
  if (options.port !== void 0 && !/^\d+$/.test(options.port)) program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`);
50
+ let tls;
51
+ if (options.tlsCert !== void 0 && options.tlsKey !== void 0) tls = {
52
+ certFile: options.tlsCert,
53
+ keyFile: options.tlsKey
54
+ };
55
+ else if (options.tlsCert !== void 0 || options.tlsKey !== void 0) program.error("error: --tls-cert and --tls-key must be supplied together");
46
56
  if (options.publicUrl !== void 0) try {
47
57
  parsePublicUrl(options.publicUrl, "--public-url");
48
58
  } catch (error) {
@@ -53,6 +63,7 @@ function apply(ctx) {
53
63
  ...options.host !== void 0 && { host: options.host },
54
64
  ...options.port !== void 0 && { port: Number(options.port) },
55
65
  ...options.publicUrl !== void 0 && { publicUrl: options.publicUrl },
66
+ ...tls !== void 0 && { tls },
56
67
  trustedHosts: options.trustedHost ?? []
57
68
  });
58
69
  });
@@ -6,9 +6,9 @@
6
6
  * config), mounts the `frontend-static` fallback owner over it, registers the
7
7
  * harness-source and web-surface prompt sections, the bash-visible web runtime
8
8
  * variable, the process-token URL line, and the default-browser handoff. An
9
- * advertised `publicUrl` replaces the published root — the loopback URL
10
- * otherwise. App command-line values arrive through the `webStartup` service
11
- * expressions in the bundle patch.
9
+ * advertised `publicUrl` replaces the published bind-address URL. Non-loopback
10
+ * HTTP binds trigger an exposure warning. App command-line values arrive
11
+ * through the `webStartup` service expressions in the bundle patch.
12
12
  * @module @deepseek-ai/dsh-web-app
13
13
  */
14
14
  import type { Context } from '@deepseek-ai/cordis';
@@ -33,33 +33,13 @@ export interface Config {
33
33
  /**
34
34
  * Canonical HTTP(S) root to advertise in the printed and opened URL,
35
35
  * `DSH_WEB_URL`, and the web-surface orientation, e.g.
36
- * `https://app.example/ui/`, normalized to end in `/`. Advertisement only;
37
- * see [public deployments](../README.md#public-deployments). Absent or YAML
38
- * `null` advertises the loopback URL.
36
+ * `https://app.example/ui/`, normalized to end in `/`. Configures no routing
37
+ * or authentication; see [public deployments](../README.md#public-deployments).
38
+ * Absent or YAML `null` advertises the bind-address URL.
39
39
  */
40
40
  publicUrl?: string;
41
- /** Explicit `--trusted-host` authorities from this invocation. */
42
- trustedHosts: string[];
43
41
  }
44
42
  export declare const Config: z<Config>;
45
- /** Bind-dependent Web values shared by the trust fence and URL display. */
46
- export interface WebRuntimeValues {
47
- /** LAN IPv4 literals sampled once when the server binds all interfaces. */
48
- lanAddresses: string[];
49
- /** LAN literals followed by explicit invocation authorities. */
50
- trustedHosts: string[];
51
- }
52
- /**
53
- * Resolve one LAN-trust snapshot from the active server bind.
54
- *
55
- * Derived entries are port-less IP literals: DNS rebinding needs an
56
- * attacker-controlled name, while an IP-literal Host is safe on any port and
57
- * an OS-assigned port is unknowable before bind.
58
- * @param bindHost - the active webserver bind host.
59
- * @param extra - explicit `--trusted-host` values, in argument order.
60
- * @returns the LAN display addresses and invocation-derived fence authorities.
61
- */
62
- export declare function resolveLanTrust(bindHost: string, extra: readonly string[]): WebRuntimeValues;
63
43
  /** Test hooks for the built dist and native browser handoff; production never mutates them. */
64
44
  export declare const internals: {
65
45
  resolveDistIndex: () => string;
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * The web app's command-line provider: it parses the `dsh --profile web` flag
3
- * family (`--host`, `--port`, `--public-url`, `--trusted-host`, `--no-open`)
3
+ * family (`--host`, `--port`, `--public-url`, `--trusted-host`, `--no-open`, TLS)
4
4
  * and its `--help` text, then provides the immutable values as
5
5
  * {@link WEB_STARTUP_SERVICE}. Ordinary rows inject that service before
6
6
  * reading it from lazy config.
7
7
  * @module @deepseek-ai/dsh-web-app/startup
8
8
  */
9
9
  import type { Context } from '@deepseek-ai/cordis';
10
+ import { type TlsConfig } from '@deepseek-ai/dsh-host-webserver';
10
11
  /** Stable Cordis plugin name. */
11
12
  export declare const name = "web-startup";
12
13
  /** Services required before the flags can be resolved. */
@@ -28,12 +29,14 @@ export interface WebStartupValues {
28
29
  publicUrl?: string;
29
30
  /** Explicit `--trusted-host` authorities, in argument order. */
30
31
  trustedHosts: string[];
32
+ /** Server certificate chain and unencrypted private key supplied together. */
33
+ tls?: TlsConfig;
31
34
  }
32
35
  /**
33
36
  * Parse and provide the Web invocation as an ordinary Cordis service. The
34
- * command's action publishes the flags this invocation named; `--host 0.0.0.0`,
35
- * a non-numeric `--port`, or a malformed `--public-url` is a usage error, so on
36
- * rejection (and on `--help`) nothing is provided.
37
+ * command's action publishes the flags this invocation named. Invalid flags,
38
+ * including an unpaired TLS certificate or key, are usage errors; rejection
39
+ * and `--help` provide no service.
37
40
  * @param ctx - plugin context carrying the command line.
38
41
  */
39
42
  export declare function apply(ctx: Context): void;