@goodandready/dsh-opencode-provider 0.5.31 → 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,17 @@
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
+
9
+ ## 0.5.32 — release 9: proxy auth and route switches
10
+
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.
12
+ - **#80 — route switches**: `goEnabled` and `consoleEnabled` are exposed in Settings. Disabling a route removes it from the provider directory, the picker and request handling at runtime; at least one route stays enabled.
13
+ - 144 tests.
14
+
3
15
  ## 0.5.31 — release 8: command localization
4
16
 
5
17
  - **#48 — `/opencode` localization**: new `commandLocale` setting (`en` default, `zh`) localizes status, usage, accounts, models, rules and errors. Exposed in Settings → OpenCode.
package/README.md CHANGED
@@ -99,7 +99,8 @@ graph LR
99
99
  - **Request heatmap** in the quota panel: a Monday-first weekday × hour grid in local time, plus a 24-hour aggregate strip underneath. Selectors switch between all requests, any single model, and any single account. A per-account table shows requests, errors and average latency/TTFT. The adapter appends `{at, provider, model, purpose?, account, reason, ok, latencyMs, ttftMs?, error?}` to `~/.dsh/storages/dsh-opencode/requests.jsonl`; no prompt, response or key is stored. Auxiliary `session-title`/`compaction` calls are excluded from the heatmap and exact duplicate records collapse. The log compacts at 2 MB to the newest 5000 lines. Collection starts with this build — the projection cache only has `lastPromptAt`, not per-request timestamps.
100
100
  - **Stable `x-opencode-session`** derived from the durable session id, so retries and every turn of a conversation reuse the same routing key.
101
101
  - **Live catalog** from `/v1/models` with a generated `models.dev` fallback.
102
- - **Proxy support**: one live `proxyUrl` setting routes inference, model discovery and usage checks through HTTP, HTTPS or SOCKS5; the dispatcher is reused until the address changes.
102
+ - **Proxy support**: one live `proxyUrl` setting routes inference, model discovery and usage checks through HTTP, HTTPS or SOCKS5; the dispatcher is reused until the address changes. `proxyUsername` and a password stored in the credential service add Basic auth for both HTTP and SOCKS5. Passwords are never returned to the browser.
103
+ - **Route switches**: enable or disable the Console/Zen and Go/Go Plus routes independently; a disabled route is removed from the picker and refuses requests. At least one route stays enabled.
103
104
  - **Per-model subscription limits** from the OpenCode Go pricing table: monthly dollar allowance plus estimated requests for the 5-hour, weekly and monthly windows, for both Go and Go Plus. Shown in the model picker and the settings page; estimates only — the gateway remains the authority.
104
105
  - **`/opencode` command** for status, usage, accounts, models, rules, and `test [model]`.
105
106
  - **TUI settings section** for the terminal profile (`/settings → OpenCode`) with the key, endpoints, proxy, allowlist, active account, fallback model, sticky routing and timeouts.
@@ -132,12 +133,12 @@ npm pack
132
133
  Install the **packed tarball** into a profile:
133
134
 
134
135
  ```bash
135
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
136
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
136
137
  # DSH Desktop:
137
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
138
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
138
139
 
139
140
  # Or install the published package:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.31
141
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.33
141
142
  ```
142
143
 
143
144
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
@@ -314,7 +315,8 @@ GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflow
314
315
  - `opencode-direct` (Console) billing shows no per-model allowance; the Go route is the primary target.
315
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.
316
317
  - The UI requires a web profile (the Host needs `ctx.webServer`); the TUI keeps the `/opencode` command and the model routes.
317
- - 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.
318
320
 
319
321
  ## License
320
322
 
package/README.ru.md CHANGED
@@ -108,7 +108,8 @@ graph LR
108
108
  - **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` (промпты и ответы не сохраняются).
109
109
  - **Стабильный заголовок `x-opencode-session`**: вычисляется из устойчивого ID сессии DSH, обеспечивая одинаковый ключ маршрутизации при повторах и диалоге.
110
110
  - **Живой каталог моделей**: автоматическое чтение `/v1/models` с резервным fallback из `models.dev`.
111
- - **Proxy**: live-параметр `proxyUrl` направляет инференс, каталог и проверку квот через HTTP, HTTPS или SOCKS5; dispatcher переиспользуется до смены адреса.
111
+ - **Proxy**: live-параметр `proxyUrl` направляет инференс, каталог и проверку квот через HTTP, HTTPS или SOCKS5; dispatcher переиспользуется до смены адреса. `proxyUsername` и пароль из credential-сервиса включают Basic auth для HTTP и SOCKS5. Пароль не возвращается в браузер.
112
+ - **Переключатели маршрутов**: Console/Zen и Go/Go Plus включаются и отключаются независимо; отключённый маршрут исчезает из выбора и отклоняет запросы. Хотя бы один маршрут остаётся включённым.
112
113
  - **Оценки лимитов подписки OpenCode Go**: долларовый лимит и расчетное число запросов для окон 5h, недели и месяца для планов Go и Go Plus.
113
114
  - **Команда `/opencode`**: быстрый просмотр статуса, квот, аккаунтов, моделей, правил и запуск теста связи `test [model]`.
114
115
  - **Настройки в терминале (TUI)**: раздел `/settings → OpenCode` для профиля terminal с ключом, эндпоинтами, allowlist, активным аккаунтом, fallback-моделью, sticky-маршрутизацией и таймаутами.
@@ -131,12 +132,12 @@ npm pack
131
132
  Установка из **упакованного tarball**:
132
133
 
133
134
  ```bash
134
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
135
136
  # DSH Desktop:
136
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
137
138
 
138
139
  # Либо установка опубликованного пакета из npm:
139
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.31
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.33
140
141
  ```
141
142
 
142
143
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
@@ -212,7 +213,8 @@ modelAccountRules:
212
213
 
213
214
  ## Безопасность и надежность
214
215
 
215
- - Запросы на изменение настроек и ключей разрешены только с 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.
216
218
  - Тексты запросов и ответов пользователей никогда не записываются в файлы журнала.
217
219
  - При исчерпании квоты на одном ключе диалог пользователя не прерывается благодаря бесшовной ротации.
218
220
 
package/README.zh.md CHANGED
@@ -106,7 +106,8 @@ graph LR
106
106
  - **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
107
107
  - **稳定的 `x-opencode-session` 标头**:根据 DSH 会话 ID 派生,确保重试与连续对话复用相同的网关路由键。
108
108
  - **实时模型目录**:自动从 `/v1/models` 同步,并以 `models.dev` 生成文件作为回退。
109
- - **代理**:一个实时 `proxyUrl` 设置即可让推理、模型目录和额度检查走 HTTP、HTTPS 或 SOCKS5;地址变化前复用 dispatcher。
109
+ - **代理**:一个实时 `proxyUrl` 设置即可让推理、模型目录和额度检查走 HTTP、HTTPS 或 SOCKS5;地址变化前复用 dispatcher。`proxyUsername` 与凭据服务中的密码可为 HTTP 和 SOCKS5 启用 Basic 认证,密码不会返回浏览器。
110
+ - **路由开关**:可独立启用或禁用 Console/Zen 与 Go/Go Plus 路由;禁用的路由会从选择器移除并拒绝请求,至少保留一个路由。
110
111
  - **Go 订阅额度估算**:每月美元额度以及 5 小时、每周和每月的估算请求次数。
111
112
  - **`/opencode` 斜杠指令**:快速查询状态、配额、账户、模型及执行联通性测试。
112
113
  - **TUI 设置**:`/settings → OpenCode` 支持密钥、端点、allowlist、活动账户、回退模型、粘性路由和超时。
@@ -126,12 +127,12 @@ npm pack
126
127
  通过**打包文件 (.tgz)**安装:
127
128
 
128
129
  ```bash
129
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
130
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
130
131
  # DSH 桌面端:
131
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
132
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.33.tgz
132
133
 
133
134
  # 或从 npm 安装已发布版本:
134
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.31
135
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.33
135
136
  ```
136
137
 
137
138
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
@@ -187,7 +188,8 @@ modelAccountRules:
187
188
 
188
189
  ## 安全规范
189
190
 
190
- - 写入与配置路由均要求 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 更新后重新审计。
191
193
  - 密钥冷却状态通过 SHA-256 哈希指纹持久化,本地日志不保存提示词与明文密钥。
192
194
 
193
195
  ## 许可证
package/lib/client.js CHANGED
@@ -190,7 +190,22 @@ window.__ModuleLoader__.load({
190
190
  commandLocale: "/opencode command language",
191
191
  commandLocaleHint: "Language for /opencode status, accounts, usage, models and rules.",
192
192
  proxyUrl: "Proxy URL",
193
- proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
193
+ proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Do not put credentials in this URL.",
194
+ proxyUsername: "Proxy username",
195
+ proxyPassword: "Proxy password",
196
+ proxyPasswordSet: "configured — enter a new one to replace it",
197
+ proxyPasswordUnset: "not configured",
198
+ proxyPasswordHint: "Stored in the credential service, never returned to the browser.",
199
+ saveProxyPassword: "Save proxy password",
200
+ clearProxyPassword: "Clear proxy password",
201
+ enabledRoutes: "Enabled routes",
202
+ routeConsole: "OpenCode Console / Zen",
203
+ routeGo: "OpenCode Go / Go Plus",
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",
194
209
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
195
210
  contextOverrideShort: "Context",
196
211
  maxOutputOverrideShort: "Max output",
@@ -426,7 +441,22 @@ window.__ModuleLoader__.load({
426
441
  commandLocale: "/opencode 命令语言",
427
442
  commandLocaleHint: "用于 /opencode 的状态、账户、用量、模型和规则输出。",
428
443
  proxyUrl: "代理地址",
429
- proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
444
+ proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。请勿在此 URL 中填写凭据。",
445
+ proxyUsername: "代理用户名",
446
+ proxyPassword: "代理密码",
447
+ proxyPasswordSet: "已配置 — 输入新密码可替换",
448
+ proxyPasswordUnset: "未配置",
449
+ proxyPasswordHint: "保存在凭据服务中,不会返回给浏览器。",
450
+ saveProxyPassword: "保存代理密码",
451
+ clearProxyPassword: "清除代理密码",
452
+ enabledRoutes: "启用的路由",
453
+ routeConsole: "OpenCode Console / Zen",
454
+ routeGo: "OpenCode Go / Go Plus",
455
+ enabledRoutesHint: "禁用的路由会从选择器中移除;至少保留一个启用的路由。",
456
+ adminToken: "管理令牌",
457
+ adminTokenHint: "此 Host 要求所有者配置的管理令牌。令牌仅保存在当前浏览器中。",
458
+ adminTokenPlaceholder: "粘贴管理令牌",
459
+ continue: "继续",
430
460
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
431
461
  contextOverrideShort: "上下文",
432
462
  maxOutputOverrideShort: "最大输出",
@@ -483,6 +513,23 @@ window.__ModuleLoader__.load({
483
513
  }
484
514
  };
485
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
+ }
486
533
  // src/client/api.ts
487
534
  async function call(path, init) {
488
535
  const response = await fetch(path, {
@@ -491,6 +538,7 @@ window.__ModuleLoader__.load({
491
538
  headers: {
492
539
  accept: "application/json",
493
540
  ...init?.body === void 0 ? {} : { "content-type": "application/json" },
541
+ ...getAdminToken() === "" ? {} : { "x-opencode-token": getAdminToken() },
494
542
  ...init?.headers ?? {}
495
543
  }
496
544
  });
@@ -556,6 +604,18 @@ window.__ModuleLoader__.load({
556
604
  body: JSON.stringify({ ref })
557
605
  });
558
606
  }
607
+ function saveProxyPassword(password) {
608
+ return call("/dsh-opencode/proxy-password", {
609
+ method: "POST",
610
+ body: JSON.stringify({ password })
611
+ });
612
+ }
613
+ function clearProxyPassword() {
614
+ return call("/dsh-opencode/proxy-password/clear", {
615
+ method: "POST",
616
+ body: "{}"
617
+ });
618
+ }
559
619
  function importAuth(entry, ref) {
560
620
  return call("/dsh-opencode/auth-import", {
561
621
  method: "POST",
@@ -1718,6 +1778,9 @@ window.__ModuleLoader__.load({
1718
1778
  requestImagePixelBudget: String(state.config.requestImagePixelBudget),
1719
1779
  requestImageMaxBytes: String(state.config.requestImageMaxBytes),
1720
1780
  proxyUrl: state.config.proxyUrl,
1781
+ proxyUsername: state.config.proxyUsername,
1782
+ goEnabled: state.config.goEnabled,
1783
+ consoleEnabled: state.config.consoleEnabled,
1721
1784
  catalogRefreshMinutes: String(state.config.catalogRefreshMinutes),
1722
1785
  commandLocale: state.config.commandLocale,
1723
1786
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
@@ -1772,6 +1835,9 @@ window.__ModuleLoader__.load({
1772
1835
  requestImagePixelBudget: numberOrNull(draft.requestImagePixelBudget) ?? 2458624,
1773
1836
  requestImageMaxBytes: numberOrNull(draft.requestImageMaxBytes) ?? 1048576,
1774
1837
  proxyUrl: draft.proxyUrl.trim(),
1838
+ proxyUsername: draft.proxyUsername.trim(),
1839
+ goEnabled: draft.goEnabled,
1840
+ consoleEnabled: draft.consoleEnabled,
1775
1841
  catalogRefreshMinutes: numberOrNull(draft.catalogRefreshMinutes) ?? 10,
1776
1842
  commandLocale: draft.commandLocale,
1777
1843
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
@@ -2014,6 +2080,9 @@ window.__ModuleLoader__.load({
2014
2080
  const [error, setError] = (0, react.useState)(null);
2015
2081
  const [notice, setNotice] = (0, react.useState)(null);
2016
2082
  const [keyDraft, setKeyDraft] = (0, react.useState)("");
2083
+ const [proxyPassword, setProxyPassword] = (0, react.useState)("");
2084
+ const [proxyBusy, setProxyBusy] = (0, react.useState)(false);
2085
+ const [tokenDraft, setTokenDraft] = (0, react.useState)("");
2017
2086
  const [keyDrafts, setKeyDrafts] = (0, react.useState)({});
2018
2087
  const [search, setSearch] = (0, react.useState)("");
2019
2088
  const [testModel, setTestModel] = (0, react.useState)("deepseek-v4.1-flash");
@@ -2077,6 +2146,34 @@ window.__ModuleLoader__.load({
2077
2146
  setSaving(false);
2078
2147
  }
2079
2148
  };
2149
+ const storeProxyPassword = async () => {
2150
+ if (proxyPassword === "") return;
2151
+ setProxyBusy(true);
2152
+ setError(null);
2153
+ try {
2154
+ await saveProxyPassword(proxyPassword);
2155
+ setProxyPassword("");
2156
+ setNotice(t("saved"));
2157
+ await load();
2158
+ } catch (caught) {
2159
+ setError(caught instanceof Error ? caught.message : t("failed"));
2160
+ } finally {
2161
+ setProxyBusy(false);
2162
+ }
2163
+ };
2164
+ const removeProxyPassword = async () => {
2165
+ setProxyBusy(true);
2166
+ setError(null);
2167
+ try {
2168
+ await clearProxyPassword();
2169
+ setNotice(t("saved"));
2170
+ await load();
2171
+ } catch (caught) {
2172
+ setError(caught instanceof Error ? caught.message : t("failed"));
2173
+ } finally {
2174
+ setProxyBusy(false);
2175
+ }
2176
+ };
2080
2177
  const checkKey = async (label) => {
2081
2178
  setChecking(label);
2082
2179
  setError(null);
@@ -2125,20 +2222,56 @@ window.__ModuleLoader__.load({
2125
2222
  className: "oc-hint",
2126
2223
  children: t("loading")
2127
2224
  });
2128
- if (state === null || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2129
- className: "oc-page",
2130
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2131
- className: "oc-error",
2132
- children: error ?? t("failed")
2133
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2134
- type: "button",
2135
- className: "oc-button",
2136
- onClick: () => {
2137
- load();
2138
- },
2139
- children: t("refresh")
2140
- })]
2141
- });
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
+ }
2142
2275
  const dirty = !safeSame(draft, draftOf(state));
2143
2276
  const activeAccount = state.accounts.find((account) => account.active);
2144
2277
  const defaultAccount = state.accounts.find((account) => account.default);
@@ -3245,6 +3378,84 @@ window.__ModuleLoader__.load({
3245
3378
  value: draft.proxyUrl,
3246
3379
  onChange: (value) => update({ proxyUrl: value })
3247
3380
  }),
3381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3382
+ label: t("proxyUsername"),
3383
+ value: draft.proxyUsername,
3384
+ onChange: (value) => update({ proxyUsername: value })
3385
+ }),
3386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3387
+ className: "oc-field",
3388
+ children: [
3389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3390
+ className: "oc-label",
3391
+ children: t("proxyPassword")
3392
+ }),
3393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3394
+ className: "oc-input",
3395
+ type: "password",
3396
+ value: proxyPassword,
3397
+ placeholder: state.proxy.configured ? t("proxyPasswordSet") : t("proxyPasswordUnset"),
3398
+ onChange: (event) => setProxyPassword(event.target.value)
3399
+ }),
3400
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3401
+ className: "oc-hint",
3402
+ children: t("proxyPasswordHint")
3403
+ }),
3404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3405
+ className: "oc-actions",
3406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3407
+ type: "button",
3408
+ className: "oc-button",
3409
+ disabled: proxyBusy || proxyPassword === "",
3410
+ onClick: () => {
3411
+ storeProxyPassword();
3412
+ },
3413
+ children: t("saveProxyPassword")
3414
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3415
+ type: "button",
3416
+ className: "oc-button",
3417
+ disabled: proxyBusy || !state.proxy.configured,
3418
+ onClick: () => {
3419
+ removeProxyPassword();
3420
+ },
3421
+ children: t("clearProxyPassword")
3422
+ })]
3423
+ })
3424
+ ]
3425
+ }),
3426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3427
+ className: "oc-field",
3428
+ children: [
3429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3430
+ className: "oc-label",
3431
+ children: t("enabledRoutes")
3432
+ }),
3433
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3434
+ className: "oc-actions",
3435
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
3436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3437
+ type: "checkbox",
3438
+ checked: draft.consoleEnabled,
3439
+ onChange: (event) => update({ consoleEnabled: event.target.checked })
3440
+ }),
3441
+ " ",
3442
+ t("routeConsole")
3443
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
3444
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3445
+ type: "checkbox",
3446
+ checked: draft.goEnabled,
3447
+ onChange: (event) => update({ goEnabled: event.target.checked })
3448
+ }),
3449
+ " ",
3450
+ t("routeGo")
3451
+ ] })]
3452
+ }),
3453
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3454
+ className: "oc-hint",
3455
+ children: t("enabledRoutesHint")
3456
+ })
3457
+ ]
3458
+ }),
3248
3459
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3249
3460
  label: t("maxOutputTokens"),
3250
3461
  type: "number",
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`. */
@@ -50,6 +52,14 @@ interface Config {
50
52
  requestImageMaxBytes?: number;
51
53
  /** HTTP/HTTPS/SOCKS5 proxy for inference, catalog and usage requests. */
52
54
  proxyUrl?: string;
55
+ /** Proxy username; the password is resolved from `proxyPasswordRef`. */
56
+ proxyUsername?: string;
57
+ /** Credential reference for the proxy password. */
58
+ proxyPasswordRef?: string;
59
+ /** Enable the Go / Go Plus subscription route. */
60
+ goEnabled?: boolean;
61
+ /** Enable the Console / Zen pay-as-you-go route. */
62
+ consoleEnabled?: boolean;
53
63
  /** Live catalog cache lifetime in minutes; defaults to 10. */
54
64
  catalogRefreshMinutes?: number;
55
65
  /** Language for `/opencode` output; defaults to English. */
@@ -93,6 +103,7 @@ declare const Config: z<Config>;
93
103
  interface ResolvedConfig {
94
104
  apiKeyEnv: string;
95
105
  apiKey?: string;
106
+ adminToken: string;
96
107
  apiBase: string;
97
108
  goApiBase: string;
98
109
  goZenBase: string;
@@ -106,6 +117,10 @@ interface ResolvedConfig {
106
117
  requestImagePixelBudget: number;
107
118
  requestImageMaxBytes: number;
108
119
  proxyUrl: string;
120
+ proxyUsername: string;
121
+ proxyPasswordRef: string;
122
+ goEnabled: boolean;
123
+ consoleEnabled: boolean;
109
124
  catalogRefreshMinutes: number;
110
125
  commandLocale: CommandLocale;
111
126
  visibleModels: readonly string[];
@@ -408,6 +423,8 @@ interface RouteDeps {
408
423
  readonly analytics?: (days: number) => Promise<AnalyticsReport>;
409
424
  readonly heatmap?: (days: number) => Promise<RequestHeatmap>;
410
425
  readonly refreshTui?: () => void;
426
+ readonly syncRoutes?: () => void;
427
+ readonly adminToken?: () => string;
411
428
  /** Test seam; defaults to the local managed-credentials registry. */
412
429
  readonly managedPath?: string;
413
430
  }
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
- import { createHash } from "node:crypto";
1
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
+ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
3
+ import { createHash, timingSafeEqual } from "node:crypto";
2
4
  import { existsSync, readFileSync, statSync } from "node:fs";
3
5
  import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
6
  import { homedir, networkInterfaces } from "node:os";
@@ -6,15 +8,14 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
8
  import { LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
7
9
  import z from "@deepseek-ai/schemastery";
8
10
  import { ProxyAgent, Socks5ProxyAgent } from "undici";
9
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
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";
14
14
  // src/proxy.ts
15
15
  /**
16
- * Proxy addresses are deliberately credential-free: the settings API returns
17
- * the configured address to the browser, so embedding a password would leak it.
16
+ * Proxy addresses stay credential-free: the settings API returns the configured
17
+ * address to the browser. Authentication is supplied separately from the
18
+ * credential service and is only assembled in memory.
18
19
  */
19
20
  function normalizeProxyUrl(raw) {
20
21
  const value = raw?.trim() ?? "";
@@ -31,7 +32,24 @@ function normalizeProxyUrl(raw) {
31
32
  "socks5:"
32
33
  ].includes(url.protocol)) throw new Error("proxy address must use http://, https:// or socks5://");
33
34
  if (url.hostname === "" || url.port === "0" || url.pathname !== "" && url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error("proxy address must contain a host and optional port, without a path, query or fragment");
34
- if (url.username !== "" || url.password !== "") throw new Error("proxy credentials are not supported; use a local proxy without authentication");
35
+ if (url.username !== "" || url.password !== "") throw new Error("proxy credentials must be set through the proxy username and password fields");
36
+ return url.href;
37
+ }
38
+ /** Validate a proxy username; the password lives in the credential service. */
39
+ function normalizeProxyUsername(raw) {
40
+ const value = raw?.trim() ?? "";
41
+ if (value === "") return "";
42
+ if (value.length > 256 || value.includes(":") || /[\u0000-\u001f\u007f]/.test(value)) throw new Error("proxy username must be 1–256 characters without a colon or control characters");
43
+ return value;
44
+ }
45
+ /** Assemble the authenticated proxy URL in memory; never persist or return it. */
46
+ function buildProxyUrl(address, username, password) {
47
+ const base = normalizeProxyUrl(address);
48
+ const user = normalizeProxyUsername(username);
49
+ if (base === "" || user === "") return base;
50
+ const url = new URL(base);
51
+ url.username = encodeURIComponent(user);
52
+ url.password = encodeURIComponent(password ?? "");
35
53
  return url.href;
36
54
  }
37
55
  /**
@@ -43,7 +61,7 @@ function createProxyFetch(address, baseFetch = fetch) {
43
61
  let disposed = false;
44
62
  const wrapped = (async (input, init) => {
45
63
  if (disposed) throw new Error("proxy transport is disposed");
46
- const proxy = normalizeProxyUrl(address());
64
+ const proxy = normalizeProxyUrl(await address());
47
65
  if (proxy === "") return baseFetch(input, init);
48
66
  let agent = agents.get(proxy);
49
67
  if (agent === void 0) {
@@ -81,7 +99,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
81
99
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
82
100
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
83
101
  const DEFAULT_AUTH_ENTRY = "opencode-go";
84
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.31";
102
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.33";
85
103
  const DEFAULT_CONTEXT_WINDOW = 262144;
86
104
  const DEFAULT_MAX_TOKENS = 32768;
87
105
  const DEFAULT_GENERATE_MAX_TOKENS = 65536;
@@ -89,6 +107,7 @@ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 25165824;
89
107
  const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2458624;
90
108
  const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1048576;
91
109
  const DEFAULT_REQUEST_PREPARATION_TIMEOUT_MS = 6e4;
110
+ const DEFAULT_PROXY_PASSWORD_REF = "OPENCODE_PROXY_PASSWORD";
92
111
  function markVolatile(schema) {
93
112
  const mark = schema.volatile;
94
113
  return typeof mark === "function" ? mark.call(schema) : schema;
@@ -96,6 +115,7 @@ function markVolatile(schema) {
96
115
  const Config = z.object({
97
116
  apiKeyEnv: markVolatile(z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV)),
98
117
  apiKey: z.string().role("secret"),
118
+ adminToken: z.string().role("secret").default(""),
99
119
  apiBase: markVolatile(z.string().default(DEFAULT_API_BASE)),
100
120
  goApiBase: markVolatile(z.string().default(DEFAULT_GO_API_BASE)),
101
121
  goZenBase: markVolatile(z.string().default(DEFAULT_GO_ZEN_BASE)),
@@ -109,6 +129,10 @@ const Config = z.object({
109
129
  requestImagePixelBudget: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET)),
110
130
  requestImageMaxBytes: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES)),
111
131
  proxyUrl: markVolatile(z.string().default("")),
132
+ proxyUsername: markVolatile(z.string().default("")),
133
+ proxyPasswordRef: markVolatile(z.string().role("credential-ref").default(DEFAULT_PROXY_PASSWORD_REF)),
134
+ goEnabled: markVolatile(z.boolean().default(true)),
135
+ consoleEnabled: markVolatile(z.boolean().default(true)),
112
136
  catalogRefreshMinutes: markVolatile(z.number().min(1).max(10080).step(1).default(10)),
113
137
  commandLocale: markVolatile(z.union(["en", "zh"]).default("en")),
114
138
  visibleModels: markVolatile(z.array(z.string()).default([])),
@@ -216,6 +240,7 @@ function resolveConfig(config) {
216
240
  return {
217
241
  apiKeyEnv: typeof config.apiKeyEnv === "string" && config.apiKeyEnv.trim() !== "" ? config.apiKeyEnv.trim() : DEFAULT_API_KEY_ENV,
218
242
  ...apiKey === void 0 ? {} : { apiKey },
243
+ adminToken: typeof config.adminToken === "string" ? config.adminToken.trim() : "",
219
244
  apiBase: typeof config.apiBase === "string" && config.apiBase !== "" ? config.apiBase : DEFAULT_API_BASE,
220
245
  goApiBase: typeof config.goApiBase === "string" && config.goApiBase !== "" ? config.goApiBase : DEFAULT_GO_API_BASE,
221
246
  goZenBase: typeof config.goZenBase === "string" && config.goZenBase !== "" ? config.goZenBase : DEFAULT_GO_ZEN_BASE,
@@ -229,6 +254,19 @@ function resolveConfig(config) {
229
254
  requestImagePixelBudget: positiveInt(config.requestImagePixelBudget, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
230
255
  requestImageMaxBytes: positiveInt(config.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
231
256
  proxyUrl: normalizeProxyUrl(config.proxyUrl),
257
+ proxyUsername: normalizeProxyUsername(config.proxyUsername),
258
+ proxyPasswordRef: typeof config.proxyPasswordRef === "string" && config.proxyPasswordRef.trim() !== "" ? config.proxyPasswordRef.trim() : DEFAULT_PROXY_PASSWORD_REF,
259
+ ...(() => {
260
+ const goEnabled = config.goEnabled !== false;
261
+ const consoleEnabled = config.consoleEnabled !== false;
262
+ return goEnabled || consoleEnabled ? {
263
+ goEnabled,
264
+ consoleEnabled
265
+ } : {
266
+ goEnabled: true,
267
+ consoleEnabled: false
268
+ };
269
+ })(),
232
270
  catalogRefreshMinutes: positiveInt(config.catalogRefreshMinutes, 10),
233
271
  commandLocale: config.commandLocale === "zh" ? "zh" : "en",
234
272
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : [],
@@ -3501,6 +3539,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3501
3539
  }
3502
3540
  async listModels(provider, opts) {
3503
3541
  const config = this.deps.config();
3542
+ if (isGoProvider(provider) ? !config.goEnabled : !config.consoleEnabled) return [];
3504
3543
  let apiKey;
3505
3544
  try {
3506
3545
  apiKey = (await this.deps.resolveKey(provider)).key;
@@ -3524,6 +3563,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3524
3563
  }
3525
3564
  async resolveModel(provider, model) {
3526
3565
  const config = this.deps.config();
3566
+ if (isGoProvider(provider) ? !config.goEnabled : !config.consoleEnabled) throw new Error(`OpenCode route "${provider}" is disabled`);
3527
3567
  const meta = effectiveModel(model, config);
3528
3568
  const maxTokens = effectiveMaxTokens(meta, config);
3529
3569
  return {
@@ -3546,6 +3586,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3546
3586
  async *run(options, fallback) {
3547
3587
  const record = this.deps.recordRequest ?? ((provider, model, purpose, account, reason, outcome) => recordRequest(provider, model, outcome?.at ?? Date.now(), purpose, account, reason, outcome));
3548
3588
  const config = this.deps.config();
3589
+ if (isGoProvider(options.provider) ? !config.goEnabled : !config.consoleEnabled) throw new Error(`OpenCode route "${options.provider}" is disabled`);
3549
3590
  const runStartedAt = Date.now();
3550
3591
  const model = effectiveModel(options.model, config);
3551
3592
  const sessionId = openCodeSessionId(options);
@@ -5035,6 +5076,10 @@ function publicConfig(config) {
5035
5076
  requestImagePixelBudget: config.requestImagePixelBudget,
5036
5077
  requestImageMaxBytes: config.requestImageMaxBytes,
5037
5078
  proxyUrl: config.proxyUrl,
5079
+ proxyUsername: config.proxyUsername,
5080
+ proxyPasswordRef: config.proxyPasswordRef,
5081
+ goEnabled: config.goEnabled,
5082
+ consoleEnabled: config.consoleEnabled,
5038
5083
  catalogRefreshMinutes: config.catalogRefreshMinutes,
5039
5084
  commandLocale: config.commandLocale,
5040
5085
  visibleModels: config.visibleModels,
@@ -5238,6 +5283,10 @@ function configOps(patch) {
5238
5283
  if ("requestImagePixelBudget" in patch) set("requestImagePixelBudget", patch.requestImagePixelBudget);
5239
5284
  if ("requestImageMaxBytes" in patch) set("requestImageMaxBytes", patch.requestImageMaxBytes);
5240
5285
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
5286
+ if ("proxyUsername" in patch) set("proxyUsername", normalizeProxyUsername(typeof patch.proxyUsername === "string" ? patch.proxyUsername : ""));
5287
+ if ("proxyPasswordRef" in patch) set("proxyPasswordRef", patch.proxyPasswordRef);
5288
+ if ("goEnabled" in patch) set("goEnabled", patch.goEnabled === true);
5289
+ if ("consoleEnabled" in patch) set("consoleEnabled", patch.consoleEnabled === true);
5241
5290
  if ("catalogRefreshMinutes" in patch) set("catalogRefreshMinutes", patch.catalogRefreshMinutes);
5242
5291
  if ("commandLocale" in patch) set("commandLocale", patch.commandLocale === "zh" ? "zh" : "en");
5243
5292
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
@@ -5277,6 +5326,12 @@ async function buildState(ctx, deps) {
5277
5326
  ...info.source === void 0 ? {} : { source: info.source }
5278
5327
  };
5279
5328
  } catch {}
5329
+ let proxyConfigured = false;
5330
+ if (config.proxyUsername !== "" && credentials !== void 0) try {
5331
+ proxyConfigured = (await credentials.describe(credentialRef(config.proxyPasswordRef))).configured;
5332
+ } catch {
5333
+ proxyConfigured = false;
5334
+ }
5280
5335
  const resolved = await deps.pool.resolveAll(GO_PROVIDER).catch(() => []);
5281
5336
  if (!keyInfo.configured) keyInfo = {
5282
5337
  ...keyInfo,
@@ -5334,9 +5389,10 @@ async function buildState(ctx, deps) {
5334
5389
  managed: slot.ref !== "" && managedRefs.has(slot.ref)
5335
5390
  });
5336
5391
  }
5392
+ const listingProvider = config.goEnabled ? GO_PROVIDER : CONSOLE_PROVIDER;
5337
5393
  let models = [];
5338
5394
  try {
5339
- models = (await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true })).map((model) => {
5395
+ models = (await deps.adapter.listModels(listingProvider, { unfiltered: true })).map((model) => {
5340
5396
  const meta = effectiveModel(model.id, config);
5341
5397
  return {
5342
5398
  id: model.id,
@@ -5351,8 +5407,8 @@ async function buildState(ctx, deps) {
5351
5407
  input: meta.input,
5352
5408
  reasoning: meta.reasoning,
5353
5409
  visible: modelVisible(model.id, config),
5354
- ...catalogReleaseDate("opencode-go-direct", model.id, config) === void 0 ? {} : { releaseDate: catalogReleaseDate(GO_PROVIDER, model.id, config) },
5355
- isNew: isNewRelease(catalogReleaseDate(GO_PROVIDER, model.id, config))
5410
+ ...catalogReleaseDate(listingProvider, model.id, config) === void 0 ? {} : { releaseDate: catalogReleaseDate(listingProvider, model.id, config) },
5411
+ isNew: isNewRelease(catalogReleaseDate(listingProvider, model.id, config))
5356
5412
  };
5357
5413
  });
5358
5414
  } catch {
@@ -5363,9 +5419,14 @@ async function buildState(ctx, deps) {
5363
5419
  return {
5364
5420
  config: publicConfig(config),
5365
5421
  key: keyInfo,
5422
+ proxy: {
5423
+ configured: proxyConfigured,
5424
+ username: config.proxyUsername,
5425
+ passwordRef: config.proxyPasswordRef
5426
+ },
5366
5427
  accounts,
5367
5428
  models,
5368
- catalog: catalogStatus(GO_PROVIDER, config, models.length),
5429
+ catalog: catalogStatus(listingProvider, config, models.length),
5369
5430
  usage: usage ?? null,
5370
5431
  service: {
5371
5432
  ...usage?.checkedAt === void 0 ? {} : { usageCheckedAt: usage.checkedAt },
@@ -5382,6 +5443,16 @@ async function buildState(ctx, deps) {
5382
5443
  revision: settingsRevision(ctx)
5383
5444
  };
5384
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
+ }
5385
5456
  /** Register the Host HTTP surface consumed by the browser settings page. */
5386
5457
  function applyRoutes(ctx, web, deps) {
5387
5458
  const disposers = [];
@@ -5389,7 +5460,16 @@ function applyRoutes(ctx, web, deps) {
5389
5460
  disposers.push(web.register({
5390
5461
  kind: "exact",
5391
5462
  path,
5392
- 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
+ }
5393
5473
  }));
5394
5474
  };
5395
5475
  route("/dsh-opencode/state", async (req, res) => {
@@ -5501,6 +5581,7 @@ function applyRoutes(ctx, web, deps) {
5501
5581
  await settings.mutate(NS, ops, revision);
5502
5582
  deps.usage.clear();
5503
5583
  deps.refreshTui?.();
5584
+ deps.syncRoutes?.();
5504
5585
  json(res, 200, { ok: true });
5505
5586
  } catch (error) {
5506
5587
  const message = error instanceof Error ? error.message : "config write failed";
@@ -5552,6 +5633,50 @@ function applyRoutes(ctx, web, deps) {
5552
5633
  json(res, 500, { error: error instanceof Error ? error.message : "key clear failed" });
5553
5634
  }
5554
5635
  });
5636
+ route("/dsh-opencode/proxy-password", async (req, res) => {
5637
+ if (!requirePost(req, res)) return;
5638
+ const credentials = getCredentials(ctx);
5639
+ if (credentials === void 0) {
5640
+ json(res, 503, { error: "credentials service unavailable" });
5641
+ return;
5642
+ }
5643
+ try {
5644
+ const body = await readJson(req);
5645
+ const password = typeof body.password === "string" ? body.password : "";
5646
+ if (password === "") {
5647
+ json(res, 400, { error: "proxy password is required" });
5648
+ return;
5649
+ }
5650
+ const refName = deps.config().proxyPasswordRef;
5651
+ await credentials.set(credentialRef(refName), password);
5652
+ await addManagedCredentialRef(refName, deps.managedPath);
5653
+ json(res, 200, {
5654
+ ok: true,
5655
+ ref: refName
5656
+ });
5657
+ } catch (error) {
5658
+ json(res, 500, { error: error instanceof Error ? error.message : "proxy password write failed" });
5659
+ }
5660
+ });
5661
+ route("/dsh-opencode/proxy-password/clear", async (req, res) => {
5662
+ if (!requirePost(req, res)) return;
5663
+ const credentials = getCredentials(ctx);
5664
+ if (credentials === void 0) {
5665
+ json(res, 503, { error: "credentials service unavailable" });
5666
+ return;
5667
+ }
5668
+ try {
5669
+ const refName = deps.config().proxyPasswordRef;
5670
+ await credentials.unset(credentialRef(refName));
5671
+ await removeManagedCredentialRef(refName, deps.managedPath);
5672
+ json(res, 200, {
5673
+ ok: true,
5674
+ ref: refName
5675
+ });
5676
+ } catch (error) {
5677
+ json(res, 500, { error: error instanceof Error ? error.message : "proxy password clear failed" });
5678
+ }
5679
+ });
5555
5680
  route("/dsh-opencode/auth-import", async (req, res) => {
5556
5681
  if (!requirePost(req, res)) return;
5557
5682
  const credentials = getCredentials(ctx);
@@ -6114,7 +6239,19 @@ const inject = ["llm"];
6114
6239
  function apply(ctx, config) {
6115
6240
  const current = () => resolveConfig(unwrapVolatileConfig(config));
6116
6241
  const keys = createKeyPool(ctx, current);
6117
- const proxy = createProxyFetch(() => current().proxyUrl);
6242
+ const proxy = createProxyFetch(async () => {
6243
+ const config = current();
6244
+ let password = "";
6245
+ if (config.proxyUsername !== "") {
6246
+ const credentials = ctx.get("credentials");
6247
+ if (credentials?.resolve !== void 0) try {
6248
+ password = (await credentials.resolve(credentialRef(config.proxyPasswordRef)))?.value ?? "";
6249
+ } catch {
6250
+ password = "";
6251
+ }
6252
+ }
6253
+ return buildProxyUrl(config.proxyUrl, config.proxyUsername, password);
6254
+ });
6118
6255
  const usage = createUsageReader(current, (provider) => keys.resolve(provider), proxy.fetch);
6119
6256
  const adapter = new OpenCodeAdapter({
6120
6257
  config: current,
@@ -6127,19 +6264,30 @@ function apply(ctx, config) {
6127
6264
  ctx.effect(() => () => {
6128
6265
  proxy.dispose();
6129
6266
  });
6130
- ctx.llm.registerConfigurableProviders([{
6267
+ const providersFor = () => {
6268
+ const config = current();
6269
+ return [...config.consoleEnabled ? [CONSOLE_PROVIDER] : [], ...config.goEnabled ? [GO_PROVIDER] : []];
6270
+ };
6271
+ const entriesFor = (providers) => [...providers.includes("opencode-direct") ? [{
6131
6272
  provider: CONSOLE_PROVIDER,
6132
6273
  displayName: "OpenCode (plugin)",
6133
6274
  settingsNs: NS,
6134
6275
  settingsPath: []
6135
- }, {
6276
+ }] : [], ...providers.includes("opencode-go-direct") ? [{
6136
6277
  provider: GO_PROVIDER,
6137
6278
  displayName: "OpenCode Go (plugin)",
6138
6279
  settingsNs: NS,
6139
6280
  settingsPath: []
6140
- }]);
6141
- ctx.llm.registerAdapter([CONSOLE_PROVIDER, GO_PROVIDER], adapter);
6142
- ctx.logger("llm-opencode").info(`registered providers: ${CONSOLE_PROVIDER}, ${GO_PROVIDER}`);
6281
+ }] : []];
6282
+ const directory = ctx.llm.registerConfigurableProviders(entriesFor(providersFor()));
6283
+ const registration = ctx.llm.registerAdapter(providersFor(), adapter);
6284
+ const syncRoutes = () => {
6285
+ const providers = providersFor();
6286
+ registration.replace(providers);
6287
+ directory.replace(entriesFor(providers));
6288
+ ctx.logger("llm-opencode").info(`enabled providers: ${providers.join(", ")}`);
6289
+ };
6290
+ ctx.logger("llm-opencode").info(`registered providers: ${providersFor().join(", ")}`);
6143
6291
  ctx.inject(["settings"], (settingsCtx) => {
6144
6292
  settingsCtx.settings.configure({ auto: false }, ctx.fiber);
6145
6293
  });
@@ -6183,7 +6331,9 @@ function apply(ctx, config) {
6183
6331
  pool: keys,
6184
6332
  usage,
6185
6333
  heatmap: readRequestHeatmap,
6186
- refreshTui: () => refreshTuiSettings?.()
6334
+ refreshTui: () => refreshTuiSettings?.(),
6335
+ syncRoutes,
6336
+ adminToken: () => current().adminToken || launchEnvironmentOf(ctx).get("OPENCODE_ADMIN_TOKEN")?.value || ""
6187
6337
  });
6188
6338
  webCtx.effect(() => registerPluginUpdater(webCtx, {
6189
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.31",
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",