@goodandready/dsh-opencode-provider 0.5.13 → 0.5.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.14
4
+
5
+ - **DSH WEB compatibility**: same-origin requests from a literal local interface address (for example `192.168.1.111`) are accepted alongside loopback. DNS-rebinding hostnames are still rejected. This restores Settings, sidebar quota, analytics and update status on DSH Web accessed at its trusted LAN URL.
6
+ - Config/key writes from that trusted same-origin host are enabled; `Origin` must match `Host`, and cross-site fetch metadata is rejected. Auto-update POST remains loopback-only.
7
+ - **UI lifecycle**: the injected style tag now uses the canonical `data-dsh-plugin` marker and lookup.
8
+ - 101 tests.
9
+
3
10
  ## 0.5.13
4
11
 
5
12
  - Removed the unused `LEGACY_CONSOLE_PROVIDER` constant and the dead `settings.models.provider-card` slot declaration left by the custom provider-card removal.
package/README.md CHANGED
@@ -130,12 +130,12 @@ npm pack
130
130
  Install the **packed tarball** into a profile:
131
131
 
132
132
  ```bash
133
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.13.tgz
133
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.14.tgz
134
134
  # DSH Desktop:
135
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.13.tgz
135
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.14.tgz
136
136
 
137
137
  # Or install the published package:
138
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.13
138
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.14
139
139
  ```
140
140
 
141
141
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
package/README.ru.md CHANGED
@@ -129,12 +129,12 @@ npm pack
129
129
  Установка из **упакованного tarball**:
130
130
 
131
131
  ```bash
132
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.13.tgz
132
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.14.tgz
133
133
  # DSH Desktop:
134
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.13.tgz
134
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.14.tgz
135
135
 
136
136
  # Либо установка опубликованного пакета из npm:
137
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.13
137
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.14
138
138
  ```
139
139
 
140
140
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
package/README.zh.md CHANGED
@@ -124,12 +124,12 @@ npm pack
124
124
  通过**打包文件 (.tgz)**安装:
125
125
 
126
126
  ```bash
127
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.13.tgz
127
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.14.tgz
128
128
  # DSH 桌面端:
129
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.13.tgz
129
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.14.tgz
130
130
 
131
131
  # 或从 npm 安装已发布版本:
132
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.13
132
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.14
133
133
  ```
134
134
 
135
135
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -2977,9 +2977,9 @@ window.__ModuleLoader__.load({
2977
2977
  //#region src/client/index.tsx
2978
2978
  function injectCss() {
2979
2979
  if (typeof document === "undefined") return () => {};
2980
- if (document.querySelector(`style[data-plugin-css="dsh-opencode-provider"]`) !== null) return () => {};
2980
+ if (document.querySelector(`style[data-dsh-plugin="dsh-opencode-provider"]`) !== null) return () => {};
2981
2981
  const tag = document.createElement("style");
2982
- tag.dataset.plugin = "dsh-opencode-provider";
2982
+ tag.dataset.dshPlugin = "dsh-opencode-provider";
2983
2983
  tag.dataset.pluginCss = CSS_ID;
2984
2984
  tag.textContent = CSS;
2985
2985
  document.head.appendChild(tag);
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
- import { homedir } from "node:os";
4
+ import { homedir, networkInterfaces } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
6
  import { LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
7
7
  import z from "@deepseek-ai/schemastery";
@@ -9,6 +9,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
9
9
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
10
10
  import { spawn } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { isIP } from "node:net";
12
13
  //#region src/config.ts
13
14
  const NS = "llm-opencode";
14
15
  const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
@@ -17,7 +18,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
17
18
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
18
19
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
19
20
  const DEFAULT_AUTH_ENTRY = "opencode-go";
20
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.13";
21
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.14";
21
22
  const DEFAULT_CONTEXT_WINDOW = 262144;
22
23
  const DEFAULT_MAX_TOKENS = 32768;
23
24
  const DEFAULT_GENERATE_MAX_TOKENS = 32768;
@@ -3854,6 +3855,48 @@ function createKeyResolver(ctx, config) {
3854
3855
  return createKeyPool(ctx, config).resolve;
3855
3856
  }
3856
3857
  //#endregion
3858
+ //#region src/local-host.ts
3859
+ const LOOPBACK = /* @__PURE__ */ new Set([
3860
+ "localhost",
3861
+ "localhost.",
3862
+ "::1",
3863
+ "[::1]"
3864
+ ]);
3865
+ function normalize(value) {
3866
+ return value.toLowerCase().replace(/^\[|\]$/g, "").replace(/%.+$/, "");
3867
+ }
3868
+ function isLoopbackHostname(value) {
3869
+ const hostname = normalize(value);
3870
+ return LOOPBACK.has(hostname) || hostname.startsWith("127.") || hostname.startsWith("::ffff:127.");
3871
+ }
3872
+ function localAddresses() {
3873
+ const addresses = /* @__PURE__ */ new Set();
3874
+ for (const entries of Object.values(networkInterfaces())) for (const entry of entries ?? []) addresses.add(normalize(entry.address));
3875
+ return addresses;
3876
+ }
3877
+ function isLocalHostname(value) {
3878
+ const hostname = normalize(value);
3879
+ if (isLoopbackHostname(hostname)) return true;
3880
+ if (isIP(hostname) === 0) return false;
3881
+ return localAddresses().has(hostname);
3882
+ }
3883
+ function authorityHostname(value) {
3884
+ if (value === void 0 || value === "") return void 0;
3885
+ try {
3886
+ return normalize(new URL(`http://${value}`).hostname);
3887
+ } catch {
3888
+ return;
3889
+ }
3890
+ }
3891
+ function sameOrigin(authority, origin) {
3892
+ try {
3893
+ const parsed = new URL(origin);
3894
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host.toLowerCase() === authority.toLowerCase();
3895
+ } catch {
3896
+ return false;
3897
+ }
3898
+ }
3899
+ //#endregion
3857
3900
  //#region src/plugin-updater.ts
3858
3901
  const UPDATE_HEADER = "x-dsh-plugin-update";
3859
3902
  const UPDATE_TIMEOUT_MS = 6e5;
@@ -3882,26 +3925,16 @@ function isTrustedUpdateRequest(request) {
3882
3925
  return false;
3883
3926
  }
3884
3927
  }
3885
- /** Local, loopback-only reads for the update card; blocks DNS-rebinding hosts. */
3928
+ /** Status reads from a local Host, including a trusted DSH Web LAN address. */
3886
3929
  function isTrustedReadRequest(request) {
3887
- if (!isLoopback(request.socket?.remoteAddress)) return false;
3888
3930
  const host = header(request, "host");
3889
3931
  if (host === void 0) return false;
3890
- try {
3891
- if (!isLoopback(new URL(`http://${host}`).hostname)) return false;
3892
- } catch {
3893
- return false;
3894
- }
3932
+ const hostname = authorityHostname(host);
3933
+ if (hostname === void 0 || !isLocalHostname(hostname)) return false;
3895
3934
  const site = header(request, "sec-fetch-site");
3896
- if (site !== void 0 && site !== "same-origin") return false;
3935
+ if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
3897
3936
  const origin = header(request, "origin");
3898
- if (origin === void 0) return true;
3899
- try {
3900
- const url = new URL(origin);
3901
- return (url.protocol === "http:" || url.protocol === "https:") && isLoopback(url.hostname) && url.host === host;
3902
- } catch {
3903
- return false;
3904
- }
3937
+ return origin === void 0 || sameOrigin(host, origin);
3905
3938
  }
3906
3939
  function validProfileName(value) {
3907
3940
  return typeof value === "string" && value !== "" && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !/[\0-\x1f\x7f]/.test(value);
@@ -4539,29 +4572,18 @@ function json(res, status, value) {
4539
4572
  function allowedOrigin(req) {
4540
4573
  const origin = req.headers.origin;
4541
4574
  if (origin === void 0 || origin === "") return true;
4542
- try {
4543
- const parsed = new URL(origin);
4544
- const host = req.headers.host;
4545
- if (host !== void 0 && parsed.host === host) return true;
4546
- return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname === "[::1]";
4547
- } catch {
4548
- return false;
4549
- }
4575
+ const host = req.headers.host;
4576
+ return host !== void 0 && sameOrigin(host, origin);
4550
4577
  }
4551
4578
  /**
4552
4579
  * DNS rebinding keeps the browser's `Host` pointing at the attacker's name
4553
- * while the socket reaches loopback. Same-origin checks alone cannot catch
4554
- * that, so write endpoints accept only a literal loopback Host.
4580
+ * while the socket reaches loopback. A trusted Host must therefore be either
4581
+ * loopback or one of this machine's literal interface addresses; a hostname
4582
+ * cannot pass on same-origin checks alone.
4555
4583
  */
4556
- function loopbackHost(req) {
4557
- const host = req.headers.host;
4558
- if (host === void 0 || host === "") return false;
4559
- try {
4560
- const hostname = new URL(`http://${host}`).hostname;
4561
- return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
4562
- } catch {
4563
- return false;
4564
- }
4584
+ function trustedHost(req) {
4585
+ const hostname = authorityHostname(req.headers.host);
4586
+ return hostname !== void 0 && isLocalHostname(hostname);
4565
4587
  }
4566
4588
  async function readJson(req) {
4567
4589
  const chunks = [];
@@ -4582,17 +4604,17 @@ function requirePost(req, res) {
4582
4604
  json(res, 405, { error: "POST required" });
4583
4605
  return false;
4584
4606
  }
4585
- if (!loopbackHost(req)) {
4586
- json(res, 403, { error: "non-loopback Host refused" });
4607
+ if (!trustedHost(req)) {
4608
+ json(res, 403, { error: "non-local Host refused" });
4587
4609
  return false;
4588
4610
  }
4589
4611
  if (!allowedOrigin(req)) {
4590
4612
  json(res, 403, { error: "cross-origin request refused" });
4591
4613
  return false;
4592
4614
  }
4593
- const remote = req.socket.remoteAddress ?? "";
4594
- if (!(remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1")) {
4595
- json(res, 403, { error: "write endpoints are restricted to the local machine" });
4615
+ const site = req.headers["sec-fetch-site"];
4616
+ if (site !== void 0 && site !== "same-origin" && site !== "none") {
4617
+ json(res, 403, { error: "cross-site request refused" });
4596
4618
  return false;
4597
4619
  }
4598
4620
  if (!(req.headers["content-type"] ?? "").includes("application/json")) {
@@ -4773,8 +4795,8 @@ function applyRoutes(ctx, web, deps) {
4773
4795
  json(res, 405, { error: "GET required" });
4774
4796
  return;
4775
4797
  }
4776
- if (!loopbackHost(req)) {
4777
- json(res, 403, { error: "non-loopback Host refused" });
4798
+ if (!trustedHost(req)) {
4799
+ json(res, 403, { error: "non-local Host refused" });
4778
4800
  return;
4779
4801
  }
4780
4802
  if (!allowedOrigin(req)) {
@@ -4792,8 +4814,8 @@ function applyRoutes(ctx, web, deps) {
4792
4814
  json(res, 405, { error: "GET required" });
4793
4815
  return;
4794
4816
  }
4795
- if (!loopbackHost(req)) {
4796
- json(res, 403, { error: "non-loopback Host refused" });
4817
+ if (!trustedHost(req)) {
4818
+ json(res, 403, { error: "non-local Host refused" });
4797
4819
  return;
4798
4820
  }
4799
4821
  if (!allowedOrigin(req)) {
@@ -4818,8 +4840,8 @@ function applyRoutes(ctx, web, deps) {
4818
4840
  json(res, 405, { error: "GET required" });
4819
4841
  return;
4820
4842
  }
4821
- if (!loopbackHost(req)) {
4822
- json(res, 403, { error: "non-loopback Host refused" });
4843
+ if (!trustedHost(req)) {
4844
+ json(res, 403, { error: "non-local Host refused" });
4823
4845
  return;
4824
4846
  }
4825
4847
  if (!allowedOrigin(req)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",