@goodandready/dsh-opencode-provider 0.5.32 → 0.5.34

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/AUDIT.md ADDED
@@ -0,0 +1,51 @@
1
+ # Project and security audit
2
+
3
+ Read-only review of `main` on 2026-11-05. Findings are recorded separately from
4
+ implementation; no production runtime or other plugin was modified during the
5
+ review.
6
+
7
+ ## Scope
8
+
9
+ - Architecture: Host routes, adapter/transports, credentials, proxy, catalog,
10
+ analytics, settings/client bundle.
11
+ - Correctness: routing, account failover, streaming, tool-call normalization,
12
+ pricing/analytics, image policy.
13
+ - Security: HTTP boundary, secret handling, updater, dependency graph.
14
+ - Tests and prior issues: 149 tests, tracked parity/security backlog.
15
+
16
+ ## Verified findings
17
+
18
+ | # | Severity | Finding | Status |
19
+ |---|---|---|---|
20
+ | A1 | High (dev-only) | `zod` 4.4.3 is pinned by every `@deepseek-ai/dsh-*` 0.2.2-rc.1 package; GHSA-mrhx-c7xc-m3w5 affects `<4.6.6`. | Residual. `npm audit --omit=dev` reports **0 vulnerabilities**; the plugin does not bundle zod. No DSH 0.2.2 release with 4.6.6 exists and no forced override is applied. Re-audit after upstream. |
21
+ | A2 | Medium (deployment) | Same-origin checks are CSRF protection, not authentication. DSH exposes no host auth for plugin routes. | Mitigated by optional `adminToken`/`OPENCODE_ADMIN_TOKEN` with constant-time comparison on every `/dsh-opencode/*` route and a browser prompt. If no token is configured, the existing loopback/same-origin guard remains; this is documented, not represented as authentication. |
22
+ | A3 | Low (operational) | Production profile `pnpm-lock.yaml` files were not regenerated during manual `node_modules` deployments. | Residual operational note. Packages installed by verified tarballs; regenerate lockfiles separately before any `--frozen-lockfile` install. |
23
+ | A4 | Low (scope) | Engine compatibility cannot run from a worktree or force-kill a runtime under project rules. | `npm run test:engine -- <candidate.tgz>` validates immutable candidates read-only; compatibility execution stays on the approved MiniPC contour. |
24
+
25
+ No critical findings were verified. No raw API keys, prompt text, image bytes
26
+ or proxy passwords are written to analytics, heatmap, request logs or sticky /
27
+ cooldown state. Credential values are masked in the UI and diagnostics.
28
+
29
+ ## Evidence
30
+
31
+ ```bash
32
+ npm run check # 149 tests + typecheck + build
33
+ npm audit --omit=dev --json # 0 production vulnerabilities
34
+ npm ls zod --all # zod 4.4.3 through DSH dev packages
35
+ npm run test:engine -- <candidate.tgz>
36
+ ```
37
+
38
+ - Production package contents are limited by `files`; no tests, sources,
39
+ credentials or worktrees are published.
40
+ - HTTP writes require a JSON body, same-origin `Origin`/`Referer`, a trusted
41
+ Host, and the admin token when configured.
42
+ - The updater is loopback-only and also sits behind the admin-token guard.
43
+ - MOA and other plugins were not touched.
44
+
45
+ ## Conclusion
46
+
47
+ The production dependency surface is clean; the only high advisory is a
48
+ dev/peer `zod` pin inherited from DSH and blocked on an upstream release. The
49
+ remaining deployment caveats (optional admin token, profile lockfiles) are
50
+ documented and tracked. No open critical or correctness issue was found in the
51
+ reviewed scope.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.34 — release 11: engine matrix and audit closure
4
+
5
+ - **#55 — engine matrix**: `npm run test:engine -- <candidate.tgz>` validates one immutable candidate against `engines.dsh` and `dsh.compatibility.dshReleases`, and rejects test/source content. It never installs DSH, starts a runtime, extracts the tarball, uses a worktree path, or performs forced cleanup; compatibility execution remains on the approved MiniPC contour.
6
+ - **#39 — project/security audit**: added `AUDIT.md` with verified findings. Production dependencies audit clean; the dev/peer `zod` advisory and optional-auth/lockfile caveats are recorded as residuals.
7
+ - **#82 — Duskriver parity**: all 18 child issues are closed; the parity parent is closed with the evidence index.
8
+ - 149 tests.
9
+
10
+ ## 0.5.33 — release 10: admin token and dependency audit
11
+
12
+ - **#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.
13
+ - **#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.
14
+ - 145 tests.
15
+
3
16
  ## 0.5.32 — release 9: proxy auth and route switches
4
17
 
5
18
  - **#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.34.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.34.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.34
142
142
  ```
143
143
 
144
144
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
@@ -304,6 +304,10 @@ npm run sync:models # regenerate src/models.generated.ts from models.dev
304
304
 
305
305
  GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflows/ci.yml`).
306
306
 
307
+ ### Engine matrix
308
+
309
+ `npm run test:engine -- <candidate.tgz>` validates one immutable, already-built package candidate against `engines.dsh` and `dsh.compatibility.dshReleases`, and checks that only production files are packaged. The command is read-only: it does not install DSH, start a runtime, extract the tarball, touch another plugin, or perform cleanup. Compatibility execution runs on the approved MiniPC test contour against the published candidate — never against a worktree path and never with forced processes or recursive deletion.
310
+
307
311
  ## Limitations
308
312
 
309
313
  - File attachments are forwarded as DSH handle text, not inline bytes; the harness normally supplies the read-only execution path. Tool-result images are inline on Anthropic, OpenAI Responses and Google; OpenAI Chat uses a placeholder.
@@ -315,7 +319,12 @@ GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflow
315
319
  - `opencode-direct` (Console) billing shows no per-model allowance; the Go route is the primary target.
316
320
  - 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
321
  - 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.
322
+ - 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.
323
+ - `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.
324
+
325
+ ## Audit
326
+
327
+ The read-only project and security review, including residual dependency and deployment findings, is in [AUDIT.md](./AUDIT.md).
319
328
 
320
329
  ## License
321
330
 
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.34.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.34.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.34
141
141
  ```
142
142
 
143
143
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
@@ -211,9 +211,14 @@ modelAccountRules:
211
211
 
212
212
  Язык вывода задаётся в **Настройки → OpenCode → `/opencode` command language** (`中文` включает китайский). По умолчанию английский.
213
213
 
214
+ ### Матрица движков
215
+
216
+ `npm run test:engine -- <candidate.tgz>` проверяет неизменяемый уже собранный пакет по `engines.dsh` и `dsh.compatibility.dshReleases`, а также состав production-файлов. Команда только читает: не ставит DSH, не запускает runtime, не распаковывает архив, не трогает другие плагины и не требует cleanup. Прогон совместимости выполняется на согласованном тестовом контуре MiniPC с опубликованным кандидатом — не из worktree и без принудительных процессов или рекурсивного удаления.
217
+
214
218
  ## Безопасность и надежность
215
219
 
216
- - Запросы на изменение настроек и ключей разрешены только с loopback (`127.0.0.1` / `::1`) с обязательной проверкой same-origin для защиты от DNS-rebinding атак.
220
+ - Same-origin — это защита от CSRF, а не аутентификация. Задайте admin-токен владельца в конфиге профиля (`adminToken`) или `OPENCODE_ADMIN_TOKEN`; тогда все маршруты `/dsh-opencode/*` требуют `x-opencode-token` или `Authorization: Bearer`. UI спросит токен один раз и сохранит его в браузере. Без токена остаются только loopback и same-origin проверки.
221
+ - `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
222
  - Тексты запросов и ответов пользователей никогда не записываются в файлы журнала.
218
223
  - При исчерпании квоты на одном ключе диалог пользователя не прерывается благодаря бесшовной ротации.
219
224
 
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.34.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.34.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.34
136
136
  ```
137
137
 
138
138
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
@@ -186,9 +186,14 @@ modelAccountRules:
186
186
 
187
187
  在 **设置 → OpenCode → `/opencode` 命令语言** 中选择 `中文` 即可切换输出语言,默认英语。
188
188
 
189
+ ### 引擎矩阵
190
+
191
+ `npm run test:engine -- <candidate.tgz>` 使用 `engines.dsh` 与 `dsh.compatibility.dshReleases` 校验不可变的已构建包,并检查只打包生产文件。该命令只读:不安装 DSH、不启动运行时、不解压归档、不修改其他插件,也不需要清理。兼容性执行在批准的 MiniPC 测试环境上使用已发布候选包完成,绝不使用 worktree 路径,也不强制结束进程或递归删除。
192
+
189
193
  ## 安全规范
190
194
 
191
- - 写入与配置路由均要求 Loopback(`127.0.0.1` / `::1`)与同源检查,防御 DNS 重新绑定。
195
+ - 同源检查用于防御 CSRF,并非身份认证。在配置文件(`adminToken`)或 `OPENCODE_ADMIN_TOKEN` 中设置管理员令牌后,所有 `/dsh-opencode/*` 路由都要求 `x-opencode-token` 或 `Authorization: Bearer`;设置界面会要求输入一次并保存在当前浏览器。未设置令牌时仍只有 Loopback 与同源检查。
196
+ - `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
197
  - 密钥冷却状态通过 SHA-256 哈希指纹持久化,本地日志不保存提示词与明文密钥。
193
198
 
194
199
  ## 许可证
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.34";
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.34",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,6 +27,7 @@
27
27
  "CHANGELOG.md",
28
28
  "LICENSE",
29
29
  "NOTICE",
30
+ "AUDIT.md",
30
31
  "media/visual-verification.png"
31
32
  ],
32
33
  "publishConfig": {
@@ -70,10 +71,11 @@
70
71
  "build": "tsdown",
71
72
  "prepare": "tsdown 1>&2",
72
73
  "typecheck": "tsc --noEmit",
73
- "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 tests/sse.test.ts tests/proxy.test.ts tests/tool-call-validation.test.ts tests/pricing.test.ts",
74
+ "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 tests/sse.test.ts tests/proxy.test.ts tests/tool-call-validation.test.ts tests/pricing.test.ts tests/engine-matrix.test.ts",
74
75
  "check": "npm run typecheck && npm test && npm run build",
75
76
  "release": "npm run check && npm pack",
76
- "sync:models": "node scripts/sync-models.mjs"
77
+ "sync:models": "node scripts/sync-models.mjs",
78
+ "test:engine": "node scripts/test-engines.mjs"
77
79
  },
78
80
  "keywords": [
79
81
  "dsh",