@goodandready/dsh-opencode-provider 0.5.13 → 0.5.15
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 +76 -48
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.15
|
|
4
|
+
|
|
5
|
+
- **Write security**: all write endpoints now require a same-origin `Origin` or `Referer` whose host matches `Host`; a request with neither is rejected with 403 instead of being treated as local. Regression tests cover missing headers, Referer fallback and the DSH LAN bridge path.
|
|
6
|
+
- **Release tooling**: `prepare` sends tsdown output to stderr, so `npm pack --json` emits parseable JSON for preflight.
|
|
7
|
+
- 102 tests.
|
|
8
|
+
|
|
9
|
+
## 0.5.14
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- 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.
|
|
13
|
+
- **UI lifecycle**: the injected style tag now uses the canonical `data-dsh-plugin` marker and lookup.
|
|
14
|
+
- 101 tests.
|
|
15
|
+
|
|
3
16
|
## 0.5.13
|
|
4
17
|
|
|
5
18
|
- 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.
|
|
133
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.15.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.15.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.15
|
|
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.15.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.15.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.15
|
|
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.15.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.15.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.15
|
|
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.15";
|
|
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,24 @@ 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
|
|
4584
|
+
function trustedHost(req) {
|
|
4585
|
+
const hostname = authorityHostname(req.headers.host);
|
|
4586
|
+
return hostname !== void 0 && isLocalHostname(hostname);
|
|
4587
|
+
}
|
|
4588
|
+
function writeOrigin(req) {
|
|
4557
4589
|
const host = req.headers.host;
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
|
|
4562
|
-
} catch {
|
|
4563
|
-
return false;
|
|
4564
|
-
}
|
|
4590
|
+
const source = req.headers.origin ?? req.headers.referer;
|
|
4591
|
+
if (host === void 0 || source === void 0 || source === "") return false;
|
|
4592
|
+
return sameOrigin(host, source);
|
|
4565
4593
|
}
|
|
4566
4594
|
async function readJson(req) {
|
|
4567
4595
|
const chunks = [];
|
|
@@ -4582,17 +4610,17 @@ function requirePost(req, res) {
|
|
|
4582
4610
|
json(res, 405, { error: "POST required" });
|
|
4583
4611
|
return false;
|
|
4584
4612
|
}
|
|
4585
|
-
if (!
|
|
4586
|
-
json(res, 403, { error: "non-
|
|
4613
|
+
if (!trustedHost(req)) {
|
|
4614
|
+
json(res, 403, { error: "non-local Host refused" });
|
|
4587
4615
|
return false;
|
|
4588
4616
|
}
|
|
4589
|
-
if (!
|
|
4590
|
-
json(res, 403, { error: "
|
|
4617
|
+
if (!writeOrigin(req)) {
|
|
4618
|
+
json(res, 403, { error: "same-origin Origin or Referer required" });
|
|
4591
4619
|
return false;
|
|
4592
4620
|
}
|
|
4593
|
-
const
|
|
4594
|
-
if (
|
|
4595
|
-
json(res, 403, { error: "
|
|
4621
|
+
const site = req.headers["sec-fetch-site"];
|
|
4622
|
+
if (site !== void 0 && site !== "same-origin" && site !== "none") {
|
|
4623
|
+
json(res, 403, { error: "cross-site request refused" });
|
|
4596
4624
|
return false;
|
|
4597
4625
|
}
|
|
4598
4626
|
if (!(req.headers["content-type"] ?? "").includes("application/json")) {
|
|
@@ -4773,8 +4801,8 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4773
4801
|
json(res, 405, { error: "GET required" });
|
|
4774
4802
|
return;
|
|
4775
4803
|
}
|
|
4776
|
-
if (!
|
|
4777
|
-
json(res, 403, { error: "non-
|
|
4804
|
+
if (!trustedHost(req)) {
|
|
4805
|
+
json(res, 403, { error: "non-local Host refused" });
|
|
4778
4806
|
return;
|
|
4779
4807
|
}
|
|
4780
4808
|
if (!allowedOrigin(req)) {
|
|
@@ -4792,8 +4820,8 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4792
4820
|
json(res, 405, { error: "GET required" });
|
|
4793
4821
|
return;
|
|
4794
4822
|
}
|
|
4795
|
-
if (!
|
|
4796
|
-
json(res, 403, { error: "non-
|
|
4823
|
+
if (!trustedHost(req)) {
|
|
4824
|
+
json(res, 403, { error: "non-local Host refused" });
|
|
4797
4825
|
return;
|
|
4798
4826
|
}
|
|
4799
4827
|
if (!allowedOrigin(req)) {
|
|
@@ -4818,8 +4846,8 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4818
4846
|
json(res, 405, { error: "GET required" });
|
|
4819
4847
|
return;
|
|
4820
4848
|
}
|
|
4821
|
-
if (!
|
|
4822
|
-
json(res, 403, { error: "non-
|
|
4849
|
+
if (!trustedHost(req)) {
|
|
4850
|
+
json(res, 403, { error: "non-local Host refused" });
|
|
4823
4851
|
return;
|
|
4824
4852
|
}
|
|
4825
4853
|
if (!allowedOrigin(req)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-opencode-provider",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.15",
|
|
4
4
|
"description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsdown",
|
|
69
|
-
"prepare": "tsdown",
|
|
69
|
+
"prepare": "tsdown 1>&2",
|
|
70
70
|
"typecheck": "tsc --noEmit",
|
|
71
71
|
"test": "node --import tsx --test tests/core.test.ts tests/routes.test.ts tests/limits.test.ts tests/analytics.test.ts tests/adapter.test.ts tests/usage.test.ts tests/request-log.test.ts tests/account-rules.test.ts tests/commands.test.ts tests/tool-schema.test.ts tests/tui-settings.test.ts tests/plan-filter.test.ts tests/image-budget.test.ts tests/tools.test.ts tests/plugin-updater.test.ts tests/client-render.test.tsx",
|
|
72
72
|
"check": "npm run typecheck && npm test && npm run build",
|