@goodandready/dsh-clinebot 0.6.25 → 0.6.26

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
@@ -5,6 +5,13 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.26] - 2026-11-05
9
+
10
+ ### Security
11
+ - **Updater proxy credentials (#285)**: proxy env values are stripped of URL userinfo before being passed to install scripts.
12
+ - **Native settings path guard (#286)**: unsafe `statsPath`, `eventLogPath`, `modelsCachePath` and `priceCatalogPath` values are ignored by startup/sync and rejected by write routes, not only by the HTTP config handler.
13
+ - **Prototype-safe price/config dictionaries (#287)**: price catalogs, config snapshots and account snapshots keep `__proto__` as own data instead of changing object prototypes.
14
+
8
15
  ## [0.6.25] - 2026-11-04
9
16
 
10
17
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>Native ClineBot / ClinePass Provider Companion for DeepSeek Harness</h3>
6
6
 
7
- <p><b>0.6.25</b> adds DSH <code>0.2.2-rc.1</code> support and keeps the security hardening from 0.6.24. It also keeps automatic model/free/dated catalog backfill, model-scoped credit cooldowns and 402/413 errors. Chat runs on the native <code>ctx.llm</code> adapter; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
7
+ <p><b>0.6.26</b> hardens updater proxy credentials, native settings data paths and prototype-safe price/config dictionaries. It keeps DSH <code>0.2.2-rc.1</code> support, automatic model/free/dated catalog backfill, model-scoped credit cooldowns and 402/413 errors. Chat runs on the native <code>ctx.llm</code> adapter; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
package/README.ru.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
6
6
 
7
- <p><b>0.6.25</b> добавляет поддержку DSH <code>0.2.2-rc.1</code> и сохраняет security-усиления 0.6.24. Также сохраняются автодобор каталога моделей/FREE/dated, cooldown недостатка кредитов для конкретной модели и ошибки 402/413. Чат работает через нативный адаптер <code>ctx.llm</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
7
+ <p><b>0.6.26</b> закрывает proxy credentials в updater, пути данных из native settings и prototype-safe словари цен/конфигурации. Сохраняются поддержка DSH <code>0.2.2-rc.1</code>, автодобор каталога моделей/FREE/dated, cooldown недостатка кредитов для конкретной модели и ошибки 402/413. Чат работает через нативный адаптер <code>ctx.llm</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>适用于 DeepSeek Harness 的 ClineBot / ClinePass 原生模型提供商伴侣插件</h3>
6
6
 
7
- <p><b>0.6.25</b> 增加 DSH <code>0.2.2-rc.1</code> 支持并保留 0.6.24 的安全加固。保留模型/免费/日期变体自动补齐、按模型隔离的余额不足冷却和明确的 402/413 错误。聊天使用原生 <code>ctx.llm</code> 适配器;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
7
+ <p><b>0.6.26</b> 加固 updater 代理凭据、native settings 数据路径和原型安全的价目/配置字典。保留 DSH <code>0.2.2-rc.1</code> 支持、模型/免费/日期变体自动补齐、按模型隔离的余额不足冷却和明确的 402/413 错误。聊天使用原生 <code>ctx.llm</code> 适配器;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
@@ -1,4 +1,5 @@
1
1
  import { resolveKeyValue, usageCache, clearUsageCache, clearProbeCache, DEFAULT_API_KEY_ENV, DEFAULT_BASE_URL, buildUsageCacheKey } from './cline-client.js'
2
+ import { safeDict } from './safe-dict.js'
2
3
 
3
4
  /**
4
5
  * Resolve all accounts in pool with their status and keys.
@@ -21,7 +22,7 @@ function arraySetting(value) {
21
22
 
22
23
  function toPlainSnapshot(cfg) {
23
24
  if (!cfg || typeof cfg !== 'object') return cfg
24
- const out = {}
25
+ const out = safeDict()
25
26
  for (const [k, v] of Object.entries(cfg)) {
26
27
  let cur = v
27
28
  while (cur && typeof cur === 'object' && !Array.isArray(cur) && typeof cur.get === 'function') {
package/lib/config.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  getActiveModelIds,
14
14
  migrateModelId,
15
15
  } from './models.js'
16
+ import { setOwn } from './safe-dict.js'
16
17
 
17
18
  // DSH settings.describe only publishes fields marked volatile. Without them the
18
19
  // namespace is omitted and the configuration page stays unavailable.
@@ -150,7 +151,7 @@ export function plainConfig(cfg) {
150
151
  }
151
152
  const out = {}
152
153
  for (const key of Object.keys(cfg)) {
153
- out[key] = plainConfig(cfg[key])
154
+ setOwn(out, key, plainConfig(cfg[key]))
154
155
  }
155
156
  return out
156
157
  }
@@ -161,7 +162,7 @@ export function volatileConfig(cfg) {
161
162
  const out = {}
162
163
  for (const [key, field] of Object.entries(Config.dict || {})) {
163
164
  if (field?.meta?.volatile && Object.hasOwn(plain, key)) {
164
- out[key] = plain[key]
165
+ setOwn(out, key, plain[key])
165
166
  }
166
167
  }
167
168
  return out
package/lib/index.js CHANGED
@@ -27,6 +27,8 @@ import { loadAccountEvents, flushAccountEvents, recordAccountEvent } from './acc
27
27
  import { setAccountEventSink } from './session-router.js'
28
28
  import { service, migrateLegacyPiAiProvider } from './services.js'
29
29
  import { applyTuiSettings } from './tui-settings.js'
30
+ import { isSafeDataPath } from './path-safety.js'
31
+ import { safeDict, setOwn } from './safe-dict.js'
30
32
 
31
33
  export const name = '@goodandready/dsh-clinebot'
32
34
  export const inject = []
@@ -42,7 +44,7 @@ export { NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rota
42
44
 
43
45
  export function apply(ctx, config) {
44
46
  let currentConfig = configReader(config)() || {}
45
- const overrides = {}
47
+ const overrides = safeDict()
46
48
  const getConfig = () => {
47
49
  if (!currentConfig) return currentConfig
48
50
  if (Object.isFrozen(currentConfig)) {
@@ -108,10 +110,11 @@ export function apply(ctx, config) {
108
110
  })
109
111
  }
110
112
 
111
- let activeStatsPath = resolvePath(publicConfig(live()).statsPath)
112
- setStatsRetentionDays(publicConfig(live()).statsRetentionDays)
113
- setPublicPriceCatalog(loadPublicPriceCache(publicConfig(live()).priceCatalogPath))
114
- let activeEventPath = resolvePath(publicConfig(live()).eventLogPath)
113
+ const initial = publicConfig(live())
114
+ let activeStatsPath = resolvePath(isSafeDataPath(initial.statsPath) ? initial.statsPath : '~/.dsh/clinebot-stats.json')
115
+ setStatsRetentionDays(initial.statsRetentionDays)
116
+ setPublicPriceCatalog(loadPublicPriceCache(isSafeDataPath(initial.priceCatalogPath) ? initial.priceCatalogPath : '~/.dsh/clinebot-prices.json'))
117
+ let activeEventPath = resolvePath(isSafeDataPath(initial.eventLogPath) ? initial.eventLogPath : '~/.dsh/clinebot-events.json')
115
118
  loadAccountEvents(activeEventPath)
116
119
  setAccountEventSink(recordAccountEvent)
117
120
  loadStats(activeStatsPath)
@@ -119,14 +122,15 @@ export function apply(ctx, config) {
119
122
  const syncStatsStorage = (cfg) => {
120
123
  const pub = publicConfig(cfg)
121
124
  setStatsRetentionDays(pub.statsRetentionDays)
122
- setPublicPriceCatalog(loadPublicPriceCache(pub.priceCatalogPath))
123
- const nextEventPath = resolvePath(pub.eventLogPath)
125
+ const priceCatalogPath = isSafeDataPath(pub.priceCatalogPath) ? pub.priceCatalogPath : '~/.dsh/clinebot-prices.json'
126
+ setPublicPriceCatalog(loadPublicPriceCache(priceCatalogPath))
127
+ const nextEventPath = resolvePath(isSafeDataPath(pub.eventLogPath) ? pub.eventLogPath : '~/.dsh/clinebot-events.json')
124
128
  if (nextEventPath !== activeEventPath) {
125
129
  flushAccountEvents()
126
130
  activeEventPath = nextEventPath
127
131
  loadAccountEvents(activeEventPath)
128
132
  }
129
- const nextPath = resolvePath(pub.statsPath)
133
+ const nextPath = resolvePath(isSafeDataPath(pub.statsPath) ? pub.statsPath : '~/.dsh/clinebot-stats.json')
130
134
  if (nextPath !== activeStatsPath) {
131
135
  switchStatsStorage(nextPath)
132
136
  activeStatsPath = nextPath
@@ -184,7 +188,7 @@ export function apply(ctx, config) {
184
188
  if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
185
189
  continue
186
190
  }
187
- overrides[key] = value
191
+ setOwn(overrides, key, value)
188
192
  }
189
193
  return
190
194
  }
@@ -202,7 +206,7 @@ export function apply(ctx, config) {
202
206
  if (desc?.set) {
203
207
  targetConfig[key] = value
204
208
  } else {
205
- targetConfig[key] = value
209
+ setOwn(targetConfig, key, value)
206
210
  }
207
211
  } catch {
208
212
  /* ignore read-only */
package/lib/price-sync.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from '
2
2
  import { homedir } from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { normalizeModelKey, getPublicPriceCatalog, setPublicPriceCatalog } from './model-prices.js'
5
+ import { safeDict } from './safe-dict.js'
5
6
 
6
7
  const DEFAULT_URL = 'https://models.dev/api.json'
7
8
 
@@ -63,7 +64,7 @@ function priority(provider) {
63
64
  * @param {Record<string, any>} data
64
65
  */
65
66
  export function buildPublicCatalog(data) {
66
- const catalog = {}
67
+ const catalog = safeDict()
67
68
  for (const provider of Object.values(data || {})) {
68
69
  if (!provider || typeof provider !== 'object') continue
69
70
  const providerId = String(provider.id || '')
@@ -102,7 +103,7 @@ export function loadPublicPriceCache(cachePath) {
102
103
  const file = expandHome(cachePath)
103
104
  if (!existsSync(file)) return {}
104
105
  const parsed = JSON.parse(readFileSync(file, 'utf8'))
105
- return parsed?.models && typeof parsed.models === 'object' ? parsed.models : {}
106
+ return parsed?.models && typeof parsed.models === 'object' ? safeDict(parsed.models) : safeDict()
106
107
  } catch {
107
108
  return {}
108
109
  }
@@ -22,6 +22,7 @@ import { publicConfig, plainConfig, Config } from './config.js'
22
22
  import { publicUsage } from './http.js'
23
23
  import { resolveKeyValue } from './credential-refs.js'
24
24
  import { resolveAccountPool, getLastRotation } from './account-pool.js'
25
+ import { isSafeDataPath } from './path-safety.js'
25
26
 
26
27
  export function resolvePathWithHome(p) {
27
28
  let guard = 0
@@ -193,7 +194,7 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
193
194
  try {
194
195
  const cfg = live()
195
196
  const pub = publicConfig(cfg)
196
- const cacheFile = resolvePathWithHome(pub.modelsCachePath)
197
+ const cacheFile = isSafeDataPath(pub.modelsCachePath) ? resolvePathWithHome(pub.modelsCachePath) : ''
197
198
  const settingsApi = getSettingsApi()
198
199
 
199
200
  // Auto-migrate obsolete IDs (e.g. deepseek-v41-flash -> deepseek-v4.1-flash)
@@ -248,7 +249,7 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
248
249
  if (isDifferent) {
249
250
  await ensurePublicPricesForModels(usageData.dynamicModels.map((model) => model.id), {
250
251
  url: pub.priceCatalogUrl,
251
- cachePath: pub.priceCatalogPath,
252
+ cachePath: isSafeDataPath(pub.priceCatalogPath) ? pub.priceCatalogPath : undefined,
252
253
  enabled: pub.priceAutoSync !== false,
253
254
  }).catch(() => {})
254
255
  }
@@ -6,6 +6,7 @@ import { getAllModels, saveModelsDiskCache, getOriginalModelContext, getOriginal
6
6
  import { fetchUsageLimits, smokeChat } from '../cline-client.js'
7
7
  import { publicUsage } from '../http.js'
8
8
  import { ensurePublicPricesForModels } from '../price-sync.js'
9
+ import { isSafeDataPath } from '../path-safety.js'
9
10
 
10
11
  export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
11
12
  // POST /dsh-clinebot/models/sync — dynamically load models from plan
@@ -51,7 +52,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
51
52
  }))
52
53
  await settingsApi.replace(next)
53
54
  await syncProviderState(next)
54
- const cacheFile = resolvePathWithHome(pub.modelsCachePath)
55
+ const cacheFile = isSafeDataPath(pub.modelsCachePath) ? resolvePathWithHome(pub.modelsCachePath) : ''
55
56
  if (cacheFile) {
56
57
  const activeAcc = await resolveActiveAccountKey(ctx, live())
57
58
  const accountRef = activeAcc?.envName || activeAcc?.apiKeyEnv || pub.activeAccount || pub.apiKeyEnv || 'CLINEBOT_API_KEY'
@@ -235,6 +235,9 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
235
235
  if (!assertTrustedSettingsRequest(req, res, ctx)) return
236
236
  try {
237
237
  const pub = publicConfig(live())
238
+ if (!isSafeDataPath(pub.statsPath)) {
239
+ return writeJson(res, 400, { ok: false, error: 'statsPath must stay inside the DSH home directory' })
240
+ }
238
241
  const stats = resetStats(pub.statsPath)
239
242
  writeJson(res, 200, { ok: true, stats })
240
243
  } catch (err) {
@@ -341,6 +344,9 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
341
344
  if (!assertTrustedSettingsRequest(req, res, ctx)) return
342
345
  try {
343
346
  const pub = publicConfig(live())
347
+ if (!isSafeDataPath(pub.priceCatalogPath)) {
348
+ return writeJson(res, 400, { ok: false, error: 'priceCatalogPath must stay inside the DSH home directory' })
349
+ }
344
350
  const result = await syncPublicPrices({
345
351
  url: pub.priceCatalogUrl,
346
352
  cachePath: pub.priceCatalogPath,
package/lib/safe-dict.js CHANGED
@@ -13,3 +13,13 @@ export function safeNestedDict(source) {
13
13
  }
14
14
  return out
15
15
  }
16
+
17
+ export function setOwn(target, key, value) {
18
+ Object.defineProperty(target, key, {
19
+ value,
20
+ writable: true,
21
+ enumerable: true,
22
+ configurable: true,
23
+ })
24
+ return target
25
+ }
@@ -9,7 +9,7 @@
9
9
  import { createRequire } from 'node:module'
10
10
 
11
11
  const require = createRequire(import.meta.url)
12
- let packageVersion = '0.6.25'
12
+ let packageVersion = '0.6.26'
13
13
  try {
14
14
  packageVersion = require('../../package.json').version || packageVersion
15
15
  } catch {
package/lib/updater.js CHANGED
@@ -25,10 +25,27 @@ const SAFE_ENV_KEYS = new Set([
25
25
  const SAFE_NPM_ENV_RE = /^npm_config_(?:registry|proxy|https_proxy|strict_ssl|userconfig|globalconfig|cache|prefix)$/i
26
26
  let latestCache = undefined
27
27
 
28
+ function sanitizeProxy(value) {
29
+ try {
30
+ const url = new URL(String(value))
31
+ url.username = ''
32
+ url.password = ''
33
+ return url.toString()
34
+ } catch {
35
+ return ''
36
+ }
37
+ }
38
+
28
39
  export function sanitizeChildEnv(env = process.env) {
29
40
  const out = {}
30
41
  for (const [key, value] of Object.entries(env)) {
31
- if (SAFE_ENV_KEYS.has(key) || SAFE_NPM_ENV_RE.test(key)) out[key] = value
42
+ if (!SAFE_ENV_KEYS.has(key) && !SAFE_NPM_ENV_RE.test(key)) continue
43
+ if (/proxy/i.test(key)) {
44
+ const clean = sanitizeProxy(value)
45
+ if (clean) out[key] = clean
46
+ } else {
47
+ out[key] = value
48
+ }
32
49
  }
33
50
  out.NO_COLOR = '1'
34
51
  return out
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
4
4
  "description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
5
5
  "license": "MIT",
6
6
  "type": "module",