@goodandready/dsh-opencode-provider 0.5.32 → 0.5.33

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.33 — release 10: admin token and dependency audit
4
+
5
+ - **#41 — authenticated HTTP routes**: optional owner `adminToken` (profile config or `OPENCODE_ADMIN_TOKEN`). When set, every `/dsh-opencode/*` route requires `x-opencode-token` or a Bearer token with constant-time comparison; forged Host/Origin alone no longer passes. The settings UI prompts for the token and keeps it in this browser.
6
+ - **#42 — zod audit**: confirmed `zod` 4.4.3 is pinned by all DSH 0.2.2-rc.1 packages; GHSA-mrhx-c7xc-m3w5 affects `<4.6.6`. No DSH release with the fix exists, this plugin does not bundle zod, and no forced override was added. Residual exposure is documented and must be re-audited after the upstream update.
7
+ - 145 tests.
8
+
3
9
  ## 0.5.32 — release 9: proxy auth and route switches
4
10
 
5
11
  - **#71 — authenticated proxy**: `proxyUsername` plus a password stored in the credential service enable Basic auth for HTTP/HTTPS and SOCKS5 proxies. The authenticated URL exists only in memory; `/state` returns the credential-free address and a `configured` flag, never the password.
package/README.md CHANGED
@@ -133,12 +133,12 @@ npm pack
133
133
  Install the **packed tarball** into a profile:
134
134
 
135
135
  ```bash
136
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
136
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
137
137
  # DSH Desktop:
138
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
138
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
139
139
 
140
140
  # Or install the published package:
141
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.32
141
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.33
142
142
  ```
143
143
 
144
144
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
@@ -315,7 +315,8 @@ GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflow
315
315
  - `opencode-direct` (Console) billing shows no per-model allowance; the Go route is the primary target.
316
316
  - The anonymous free lane (`Bearer public` + CLI impersonation) is not implemented. A keyless `big-pickle` probe on 2026-11-05 returned HTTP 429 `FreeUsageLimitError`; use the keyed Console/Zen route for free models.
317
317
  - The UI requires a web profile (the Host needs `ctx.webServer`); the TUI keeps the `/opencode` command and the model routes.
318
- - Write routes are loopback-only; a web profile exposed to a LAN can read state but cannot save configuration or keys until the host auth token is integrated.
318
+ - Same-origin checks are CSRF protection, not authentication. Set an owner admin token in the profile config (`adminToken`) or `OPENCODE_ADMIN_TOKEN`; every `/dsh-opencode/*` route then requires `x-opencode-token` or `Authorization: Bearer`. The settings UI asks for the token once and keeps it in this browser. Without a token, routes retain the loopback/same-origin guard only.
319
+ - `npm audit` still reports `zod < 4.6.6` (GHSA-mrhx-c7xc-m3w5) because every `@deepseek-ai/dsh-*` 0.2.2-rc.1 package pins `zod` 4.4.3. No DSH 0.2.2 release with 4.6.6 exists yet; this plugin does not bundle zod and no forced override is used. Re-audit after the upstream DSH update.
319
320
 
320
321
  ## License
321
322
 
package/README.ru.md CHANGED
@@ -132,12 +132,12 @@ npm pack
132
132
  Установка из **упакованного tarball**:
133
133
 
134
134
  ```bash
135
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
136
136
  # DSH Desktop:
137
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
138
138
 
139
139
  # Либо установка опубликованного пакета из npm:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.32
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.33
141
141
  ```
142
142
 
143
143
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
@@ -213,7 +213,8 @@ modelAccountRules:
213
213
 
214
214
  ## Безопасность и надежность
215
215
 
216
- - Запросы на изменение настроек и ключей разрешены только с loopback (`127.0.0.1` / `::1`) с обязательной проверкой same-origin для защиты от DNS-rebinding атак.
216
+ - Same-origin — это защита от CSRF, а не аутентификация. Задайте admin-токен владельца в конфиге профиля (`adminToken`) или `OPENCODE_ADMIN_TOKEN`; тогда все маршруты `/dsh-opencode/*` требуют `x-opencode-token` или `Authorization: Bearer`. UI спросит токен один раз и сохранит его в браузере. Без токена остаются только loopback и same-origin проверки.
217
+ - `npm audit` по-прежнему показывает `zod < 4.6.6` (GHSA-mrhx-c7xc-m3w5): пакеты `@deepseek-ai/dsh-*` 0.2.2-rc.1 фиксируют zod 4.4.3. Релиза DSH 0.2.2 с 4.6.6 пока нет; плагин не бандлит zod и не применяет принудительный override. Повторить аудит после обновления DSH.
217
218
  - Тексты запросов и ответов пользователей никогда не записываются в файлы журнала.
218
219
  - При исчерпании квоты на одном ключе диалог пользователя не прерывается благодаря бесшовной ротации.
219
220
 
package/README.zh.md CHANGED
@@ -127,12 +127,12 @@ npm pack
127
127
  通过**打包文件 (.tgz)**安装:
128
128
 
129
129
  ```bash
130
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
130
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
131
131
  # DSH 桌面端:
132
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
132
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
133
133
 
134
134
  # 或从 npm 安装已发布版本:
135
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.32
135
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.33
136
136
  ```
137
137
 
138
138
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
@@ -188,7 +188,8 @@ modelAccountRules:
188
188
 
189
189
  ## 安全规范
190
190
 
191
- - 写入与配置路由均要求 Loopback(`127.0.0.1` / `::1`)与同源检查,防御 DNS 重新绑定。
191
+ - 同源检查用于防御 CSRF,并非身份认证。在配置文件(`adminToken`)或 `OPENCODE_ADMIN_TOKEN` 中设置管理员令牌后,所有 `/dsh-opencode/*` 路由都要求 `x-opencode-token` 或 `Authorization: Bearer`;设置界面会要求输入一次并保存在当前浏览器。未设置令牌时仍只有 Loopback 与同源检查。
192
+ - `npm audit` 仍报告 `zod < 4.6.6`(GHSA-mrhx-c7xc-m3w5):`@deepseek-ai/dsh-*` 0.2.2-rc.1 固定使用 zod 4.4.3。目前没有使用 4.6.6 的 DSH 0.2.2 版本;本插件不打包 zod,也不使用强制 override。待上游 DSH 更新后重新审计。
192
193
  - 密钥冷却状态通过 SHA-256 哈希指纹持久化,本地日志不保存提示词与明文密钥。
193
194
 
194
195
  ## 许可证
package/lib/client.js CHANGED
@@ -202,6 +202,10 @@ window.__ModuleLoader__.load({
202
202
  routeConsole: "OpenCode Console / Zen",
203
203
  routeGo: "OpenCode Go / Go Plus",
204
204
  enabledRoutesHint: "Disabled routes are removed from the picker; at least one route stays enabled.",
205
+ adminToken: "Admin token",
206
+ adminTokenHint: "This Host requires the admin token configured by its owner. It is stored in this browser only.",
207
+ adminTokenPlaceholder: "Paste the admin token",
208
+ continue: "Continue",
205
209
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
206
210
  contextOverrideShort: "Context",
207
211
  maxOutputOverrideShort: "Max output",
@@ -449,6 +453,10 @@ window.__ModuleLoader__.load({
449
453
  routeConsole: "OpenCode Console / Zen",
450
454
  routeGo: "OpenCode Go / Go Plus",
451
455
  enabledRoutesHint: "禁用的路由会从选择器中移除;至少保留一个启用的路由。",
456
+ adminToken: "管理令牌",
457
+ adminTokenHint: "此 Host 要求所有者配置的管理令牌。令牌仅保存在当前浏览器中。",
458
+ adminTokenPlaceholder: "粘贴管理令牌",
459
+ continue: "继续",
452
460
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
453
461
  contextOverrideShort: "上下文",
454
462
  maxOutputOverrideShort: "最大输出",
@@ -505,6 +513,23 @@ window.__ModuleLoader__.load({
505
513
  }
506
514
  };
507
515
  const GO_PROVIDER = "opencode-go-direct";
516
+ // src/client/admin-token.ts
517
+ const KEY = "dsh-opencode-admin-token";
518
+ function storage() {
519
+ try {
520
+ return typeof localStorage === "undefined" ? void 0 : localStorage;
521
+ } catch {
522
+ return;
523
+ }
524
+ }
525
+ function getAdminToken() {
526
+ return storage()?.getItem(KEY) ?? "";
527
+ }
528
+ function setAdminToken(token) {
529
+ const value = token.trim();
530
+ if (value === "") storage()?.removeItem(KEY);
531
+ else storage()?.setItem(KEY, value);
532
+ }
508
533
  // src/client/api.ts
509
534
  async function call(path, init) {
510
535
  const response = await fetch(path, {
@@ -513,6 +538,7 @@ window.__ModuleLoader__.load({
513
538
  headers: {
514
539
  accept: "application/json",
515
540
  ...init?.body === void 0 ? {} : { "content-type": "application/json" },
541
+ ...getAdminToken() === "" ? {} : { "x-opencode-token": getAdminToken() },
516
542
  ...init?.headers ?? {}
517
543
  }
518
544
  });
@@ -2056,6 +2082,7 @@ window.__ModuleLoader__.load({
2056
2082
  const [keyDraft, setKeyDraft] = (0, react.useState)("");
2057
2083
  const [proxyPassword, setProxyPassword] = (0, react.useState)("");
2058
2084
  const [proxyBusy, setProxyBusy] = (0, react.useState)(false);
2085
+ const [tokenDraft, setTokenDraft] = (0, react.useState)("");
2059
2086
  const [keyDrafts, setKeyDrafts] = (0, react.useState)({});
2060
2087
  const [search, setSearch] = (0, react.useState)("");
2061
2088
  const [testModel, setTestModel] = (0, react.useState)("deepseek-v4.1-flash");
@@ -2195,20 +2222,56 @@ window.__ModuleLoader__.load({
2195
2222
  className: "oc-hint",
2196
2223
  children: t("loading")
2197
2224
  });
2198
- if (state === null || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2199
- className: "oc-page",
2200
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2201
- className: "oc-error",
2202
- children: error ?? t("failed")
2203
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2204
- type: "button",
2205
- className: "oc-button",
2206
- onClick: () => {
2207
- load();
2208
- },
2209
- children: t("refresh")
2210
- })]
2211
- });
2225
+ if (state === null || draft === null) {
2226
+ if (error?.includes("admin token required") === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2227
+ className: "oc-page",
2228
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2229
+ className: "oc-card",
2230
+ children: [
2231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2232
+ className: "oc-card-title",
2233
+ children: t("adminToken")
2234
+ }),
2235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2236
+ className: "oc-hint",
2237
+ children: t("adminTokenHint")
2238
+ }),
2239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2240
+ className: "oc-input",
2241
+ type: "password",
2242
+ value: tokenDraft,
2243
+ placeholder: t("adminTokenPlaceholder"),
2244
+ onChange: (event) => setTokenDraft(event.target.value)
2245
+ }),
2246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2247
+ type: "button",
2248
+ className: "oc-button",
2249
+ disabled: tokenDraft.trim() === "",
2250
+ onClick: () => {
2251
+ setAdminToken(tokenDraft);
2252
+ setTokenDraft("");
2253
+ load();
2254
+ },
2255
+ children: t("continue")
2256
+ })
2257
+ ]
2258
+ })
2259
+ });
2260
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2261
+ className: "oc-page",
2262
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2263
+ className: "oc-error",
2264
+ children: error ?? t("failed")
2265
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2266
+ type: "button",
2267
+ className: "oc-button",
2268
+ onClick: () => {
2269
+ load();
2270
+ },
2271
+ children: t("refresh")
2272
+ })]
2273
+ });
2274
+ }
2212
2275
  const dirty = !safeSame(draft, draftOf(state));
2213
2276
  const activeAccount = state.accounts.find((account) => account.active);
2214
2277
  const defaultAccount = state.accounts.find((account) => account.default);
package/lib/index.d.ts CHANGED
@@ -24,6 +24,8 @@ interface Config {
24
24
  apiKeyEnv?: string;
25
25
  /** Literal key for composition config only; stripped from settings reads by `role('secret')`. */
26
26
  apiKey?: string;
27
+ /** Optional admin token for the Host HTTP routes; also read from `OPENCODE_ADMIN_TOKEN`. */
28
+ adminToken?: string;
27
29
  /** Console/Zen inference root; defaults to `https://opencode.ai/inference`. */
28
30
  apiBase?: string;
29
31
  /** Go inference root; defaults to `https://opencode.ai/inference/go`. */
@@ -101,6 +103,7 @@ declare const Config: z<Config>;
101
103
  interface ResolvedConfig {
102
104
  apiKeyEnv: string;
103
105
  apiKey?: string;
106
+ adminToken: string;
104
107
  apiBase: string;
105
108
  goApiBase: string;
106
109
  goZenBase: string;
@@ -421,6 +424,7 @@ interface RouteDeps {
421
424
  readonly heatmap?: (days: number) => Promise<RequestHeatmap>;
422
425
  readonly refreshTui?: () => void;
423
426
  readonly syncRoutes?: () => void;
427
+ readonly adminToken?: () => string;
424
428
  /** Test seam; defaults to the local managed-credentials registry. */
425
429
  readonly managedPath?: string;
426
430
  }
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
- import { createHash } from "node:crypto";
2
+ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
3
+ import { createHash, timingSafeEqual } from "node:crypto";
3
4
  import { existsSync, readFileSync, statSync } from "node:fs";
4
5
  import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
6
  import { homedir, networkInterfaces } from "node:os";
@@ -7,7 +8,6 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
8
  import { LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
8
9
  import z from "@deepseek-ai/schemastery";
9
10
  import { ProxyAgent, Socks5ProxyAgent } from "undici";
10
- import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
11
11
  import { spawn } from "node:child_process";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { isIP } from "node:net";
@@ -99,7 +99,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
99
99
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
100
100
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
101
101
  const DEFAULT_AUTH_ENTRY = "opencode-go";
102
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.32";
102
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.33";
103
103
  const DEFAULT_CONTEXT_WINDOW = 262144;
104
104
  const DEFAULT_MAX_TOKENS = 32768;
105
105
  const DEFAULT_GENERATE_MAX_TOKENS = 65536;
@@ -115,6 +115,7 @@ function markVolatile(schema) {
115
115
  const Config = z.object({
116
116
  apiKeyEnv: markVolatile(z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV)),
117
117
  apiKey: z.string().role("secret"),
118
+ adminToken: z.string().role("secret").default(""),
118
119
  apiBase: markVolatile(z.string().default(DEFAULT_API_BASE)),
119
120
  goApiBase: markVolatile(z.string().default(DEFAULT_GO_API_BASE)),
120
121
  goZenBase: markVolatile(z.string().default(DEFAULT_GO_ZEN_BASE)),
@@ -239,6 +240,7 @@ function resolveConfig(config) {
239
240
  return {
240
241
  apiKeyEnv: typeof config.apiKeyEnv === "string" && config.apiKeyEnv.trim() !== "" ? config.apiKeyEnv.trim() : DEFAULT_API_KEY_ENV,
241
242
  ...apiKey === void 0 ? {} : { apiKey },
243
+ adminToken: typeof config.adminToken === "string" ? config.adminToken.trim() : "",
242
244
  apiBase: typeof config.apiBase === "string" && config.apiBase !== "" ? config.apiBase : DEFAULT_API_BASE,
243
245
  goApiBase: typeof config.goApiBase === "string" && config.goApiBase !== "" ? config.goApiBase : DEFAULT_GO_API_BASE,
244
246
  goZenBase: typeof config.goZenBase === "string" && config.goZenBase !== "" ? config.goZenBase : DEFAULT_GO_ZEN_BASE,
@@ -5441,6 +5443,16 @@ async function buildState(ctx, deps) {
5441
5443
  revision: settingsRevision(ctx)
5442
5444
  };
5443
5445
  }
5446
+ function adminAllowed(req, deps) {
5447
+ const expected = deps.adminToken?.() ?? "";
5448
+ if (expected === "") return true;
5449
+ const header = req.headers["x-opencode-token"];
5450
+ const authorization = req.headers.authorization;
5451
+ const provided = typeof header === "string" && header !== "" ? header : typeof authorization === "string" && authorization.startsWith("Bearer ") ? authorization.slice(7).trim() : "";
5452
+ const left = Buffer.from(expected);
5453
+ const right = Buffer.from(provided);
5454
+ return left.length === right.length && timingSafeEqual(left, right);
5455
+ }
5444
5456
  /** Register the Host HTTP surface consumed by the browser settings page. */
5445
5457
  function applyRoutes(ctx, web, deps) {
5446
5458
  const disposers = [];
@@ -5448,7 +5460,16 @@ function applyRoutes(ctx, web, deps) {
5448
5460
  disposers.push(web.register({
5449
5461
  kind: "exact",
5450
5462
  path,
5451
- handler
5463
+ handler: async (req, res) => {
5464
+ if (!adminAllowed(req, deps)) {
5465
+ json(res, 401, {
5466
+ error: "admin token required",
5467
+ authRequired: true
5468
+ });
5469
+ return;
5470
+ }
5471
+ await handler(req, res);
5472
+ }
5452
5473
  }));
5453
5474
  };
5454
5475
  route("/dsh-opencode/state", async (req, res) => {
@@ -6311,7 +6332,8 @@ function apply(ctx, config) {
6311
6332
  usage,
6312
6333
  heatmap: readRequestHeatmap,
6313
6334
  refreshTui: () => refreshTuiSettings?.(),
6314
- syncRoutes
6335
+ syncRoutes,
6336
+ adminToken: () => current().adminToken || launchEnvironmentOf(ctx).get("OPENCODE_ADMIN_TOKEN")?.value || ""
6315
6337
  });
6316
6338
  webCtx.effect(() => registerPluginUpdater(webCtx, {
6317
6339
  endpoint: "/dsh-opencode/update",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.32",
3
+ "version": "0.5.33",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",