@goodandready/dsh-opencode-provider 0.5.12 → 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 +13 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +2 -2
- package/lib/index.js +69 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.5.13
|
|
11
|
+
|
|
12
|
+
- Removed the unused `LEGACY_CONSOLE_PROVIDER` constant and the dead `settings.models.provider-card` slot declaration left by the custom provider-card removal.
|
|
13
|
+
- Removed a stray zero-byte `$null` file from the repository root.
|
|
14
|
+
- No runtime behavior change; 100 tests, typecheck and build pass.
|
|
15
|
+
|
|
3
16
|
## 0.5.12
|
|
4
17
|
|
|
5
18
|
- **Security**: read routes now require a literal loopback Host; `/state` is GET-only, `/health` and `/analytics` reject DNS-rebinding hosts, and updater status GET uses a loopback read guard. Added regression tests.
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
2980
|
+
if (document.querySelector(`style[data-dsh-plugin="dsh-opencode-provider"]`) !== null) return () => {};
|
|
2981
2981
|
const tag = document.createElement("style");
|
|
2982
|
-
tag.dataset.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
3891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4543
|
-
|
|
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.
|
|
4554
|
-
*
|
|
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
|
|
4557
|
-
const
|
|
4558
|
-
|
|
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 (!
|
|
4586
|
-
json(res, 403, { error: "non-
|
|
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
|
|
4594
|
-
if (
|
|
4595
|
-
json(res, 403, { error: "
|
|
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 (!
|
|
4777
|
-
json(res, 403, { error: "non-
|
|
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 (!
|
|
4796
|
-
json(res, 403, { error: "non-
|
|
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 (!
|
|
4822
|
-
json(res, 403, { error: "non-
|
|
4843
|
+
if (!trustedHost(req)) {
|
|
4844
|
+
json(res, 403, { error: "non-local Host refused" });
|
|
4823
4845
|
return;
|
|
4824
4846
|
}
|
|
4825
4847
|
if (!allowedOrigin(req)) {
|