@goodandready/dsh-clinebot 0.6.23 → 0.6.24
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 +8 -0
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/http.js +5 -0
- package/lib/path-safety.js +22 -4
- package/lib/safe-dict.js +15 -0
- package/lib/stats-storage.js +18 -17
- package/lib/telemetry.js +8 -4
- package/lib/transport/cline-transport.js +1 -1
- package/lib/updater.js +13 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ 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.24] - 2026-10-28
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- **Fail-closed source gate** (#250): without `connection.requestRejection`, remote requests are rejected regardless of spoofable `Origin`/`Referer`/`Sec-Fetch-Site`/`X-Forwarded-Host`; loopback remains allowed.
|
|
12
|
+
- **Allowlisted updater env** (#267): the child install process receives only safe OS/npm/DSH variables, not API keys, auth tokens, proxy credentials or other secrets.
|
|
13
|
+
- **Symlink-safe data paths** (#269): realpath/ancestor resolution rejects symlink escapes and profile/config targets.
|
|
14
|
+
- **Prototype-safe telemetry** (#281): stats and session dictionaries use null-prototype maps; `__proto__` session/model keys no longer mutate `Object.prototype`.
|
|
15
|
+
|
|
8
16
|
## [0.6.23] - 2026-10-28
|
|
9
17
|
|
|
10
18
|
### Fixed
|
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.
|
|
7
|
+
<p><b>0.6.24</b> hardens the source gate, updater environment, symlink-safe data paths and prototype-safe telemetry. It keeps automatic model/free/dated catalog backfill, DSH <code>0.2.1-rc.4</code>, 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.
|
|
7
|
+
<p><b>0.6.24</b> ужесточает source gate, окружение updater, symlink-безопасность data paths и защиту телеметрии от prototype pollution. Сохраняются автодобор каталога моделей/FREE/dated, DSH <code>0.2.1-rc.4</code>, 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.
|
|
7
|
+
<p><b>0.6.24</b> 加固 source gate、更新器环境、符号链接安全的数据路径和原型污染安全的遥测。保留模型/免费/日期变体自动补齐、DSH <code>0.2.1-rc.4</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>
|
package/lib/http.js
CHANGED
|
@@ -87,6 +87,11 @@ export function isTrustedSettingsRequest(request, ctx = null) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
const remoteLoopback = isLoopbackAddress(request?.socket?.remoteAddress)
|
|
90
|
+
const hasConnectionAuth = Boolean(conn && typeof conn.requestRejection === 'function')
|
|
91
|
+
// Without the host auth service, a remote request cannot prove it is a
|
|
92
|
+
// same-origin browser request: Origin/Referer/Sec-Fetch are caller-controlled.
|
|
93
|
+
if (!hasConnectionAuth && !remoteLoopback) return false
|
|
94
|
+
|
|
90
95
|
// A forwarding header is only trusted when the actual peer is the local
|
|
91
96
|
// reverse proxy; never trust it from a LAN/remote client.
|
|
92
97
|
const forwardedHost = header(request, 'x-forwarded-host')
|
package/lib/path-safety.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
1
2
|
import os from 'node:os'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
|
|
@@ -5,6 +6,22 @@ export function dshHome() {
|
|
|
5
6
|
return path.resolve(process.env.DSH_HOME || path.join(process.env.HOME || os.homedir(), '.dsh'))
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
function realPath(target) {
|
|
10
|
+
let current = target
|
|
11
|
+
const missing = []
|
|
12
|
+
while (!fs.existsSync(current)) {
|
|
13
|
+
const parent = path.dirname(current)
|
|
14
|
+
if (parent === current) break
|
|
15
|
+
missing.unshift(path.basename(current))
|
|
16
|
+
current = parent
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return path.join(fs.realpathSync(current), ...missing)
|
|
20
|
+
} catch {
|
|
21
|
+
return target
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
export function isSafeDataPath(value) {
|
|
9
26
|
const raw = String(value ?? '').trim()
|
|
10
27
|
if (!raw) return true
|
|
@@ -12,11 +29,12 @@ export function isSafeDataPath(value) {
|
|
|
12
29
|
const resolved = raw === '~' || raw.startsWith('~/')
|
|
13
30
|
? path.resolve(process.env.HOME || os.homedir(), raw.slice(1))
|
|
14
31
|
: path.resolve(raw)
|
|
15
|
-
const home = dshHome()
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
const home = realPath(dshHome())
|
|
33
|
+
const realResolved = realPath(resolved)
|
|
34
|
+
if (realResolved === home || !realResolved.startsWith(`${home}${path.sep}`)) return false
|
|
35
|
+
const relative = path.relative(home, realResolved).toLowerCase()
|
|
18
36
|
const segments = relative.split(/[\\/]+/)
|
|
19
|
-
const base = path.basename(
|
|
37
|
+
const base = path.basename(realResolved).toLowerCase()
|
|
20
38
|
if (segments[0] === 'profiles' && !base.startsWith('clinebot-')) return false
|
|
21
39
|
if (path.extname(base) !== '.json') return false
|
|
22
40
|
return !['package.json', 'package-lock.json', 'tsconfig.json', 'settings.json', '.credentials.json'].includes(base)
|
package/lib/safe-dict.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function safeDict(source) {
|
|
2
|
+
const out = Object.create(null)
|
|
3
|
+
if (source && typeof source === 'object') {
|
|
4
|
+
for (const key of Object.keys(source)) out[key] = source[key]
|
|
5
|
+
}
|
|
6
|
+
return out
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function safeNestedDict(source) {
|
|
10
|
+
const out = Object.create(null)
|
|
11
|
+
if (source && typeof source === 'object') {
|
|
12
|
+
for (const key of Object.keys(source)) out[key] = safeDict(source[key])
|
|
13
|
+
}
|
|
14
|
+
return out
|
|
15
|
+
}
|
package/lib/stats-storage.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
pruneRetentionOnStats,
|
|
16
16
|
} from './stats-metrics.js'
|
|
17
17
|
import { csvCell } from './csv.js'
|
|
18
|
+
import { safeDict, safeNestedDict } from './safe-dict.js'
|
|
18
19
|
|
|
19
20
|
const MAX_SESSIONS = 500
|
|
20
21
|
const SESSION_TTL_MS = 90 * 24 * 60 * 60 * 1000
|
|
@@ -69,13 +70,13 @@ export function createEmptyStats() {
|
|
|
69
70
|
createdAt: Date.now(),
|
|
70
71
|
updatedAt: Date.now(),
|
|
71
72
|
totals: emptyBucket(),
|
|
72
|
-
byDay:
|
|
73
|
-
byMonth:
|
|
74
|
-
byModel:
|
|
75
|
-
byAccount:
|
|
76
|
-
bySession:
|
|
77
|
-
byHour:
|
|
78
|
-
byHourModel:
|
|
73
|
+
byDay: safeDict(),
|
|
74
|
+
byMonth: safeDict(),
|
|
75
|
+
byModel: safeDict(),
|
|
76
|
+
byAccount: safeDict(),
|
|
77
|
+
bySession: safeDict(),
|
|
78
|
+
byHour: safeDict(),
|
|
79
|
+
byHourModel: safeNestedDict(),
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -131,13 +132,13 @@ export function loadStats(customPath) {
|
|
|
131
132
|
...parsed,
|
|
132
133
|
version: 2,
|
|
133
134
|
totals: { ...empty.totals, ...parsed.totals },
|
|
134
|
-
byDay: parsed.byDay
|
|
135
|
-
byMonth: parsed.byMonth
|
|
136
|
-
byModel: parsed.byModel
|
|
137
|
-
byAccount: parsed.byAccount
|
|
138
|
-
bySession: parsed.bySession
|
|
139
|
-
byHour: parsed.byHour
|
|
140
|
-
byHourModel: parsed.byHourModel
|
|
135
|
+
byDay: safeDict(parsed.byDay),
|
|
136
|
+
byMonth: safeDict(parsed.byMonth),
|
|
137
|
+
byModel: safeDict(parsed.byModel),
|
|
138
|
+
byAccount: safeDict(parsed.byAccount),
|
|
139
|
+
bySession: safeDict(parsed.bySession),
|
|
140
|
+
byHour: safeDict(parsed.byHour),
|
|
141
|
+
byHourModel: safeNestedDict(parsed.byHourModel),
|
|
141
142
|
}
|
|
142
143
|
backfillHourlyFromSessions()
|
|
143
144
|
pruneRetentionOnStats(inMemoryStats, statsRetentionDays)
|
|
@@ -237,7 +238,7 @@ function addToBucket(bucket, entry) {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
function incrementBucket(container, key, entry) {
|
|
240
|
-
if (!container
|
|
241
|
+
if (!Object.hasOwn(container, key)) container[key] = emptyBucket()
|
|
241
242
|
addToBucket(container[key], entry)
|
|
242
243
|
}
|
|
243
244
|
|
|
@@ -298,7 +299,7 @@ function backfillHourlyFromSessions() {
|
|
|
298
299
|
if (bucket.model) hour.model = bucket.model
|
|
299
300
|
if (bucket.accountId) hour.accountId = bucket.accountId
|
|
300
301
|
const model = bucket.model || 'unknown'
|
|
301
|
-
if (!inMemoryStats.byHourModel
|
|
302
|
+
if (!Object.hasOwn(inMemoryStats.byHourModel, key)) inMemoryStats.byHourModel[key] = safeDict()
|
|
302
303
|
inMemoryStats.byHourModel[key][model] = Number(inMemoryStats.byHourModel[key][model] || 0) + requests
|
|
303
304
|
}
|
|
304
305
|
}
|
|
@@ -359,7 +360,7 @@ export function recordUsage({
|
|
|
359
360
|
const localNow = new Date(now)
|
|
360
361
|
const hourKey = `${localDateKey(localNow)}T${String(localNow.getHours()).padStart(2, '0')}`
|
|
361
362
|
incrementBucket(inMemoryStats.byHour, hourKey, entry)
|
|
362
|
-
if (!inMemoryStats.byHourModel
|
|
363
|
+
if (!Object.hasOwn(inMemoryStats.byHourModel, hourKey)) inMemoryStats.byHourModel[hourKey] = safeDict()
|
|
363
364
|
inMemoryStats.byHourModel[hourKey][model] = Number(inMemoryStats.byHourModel[hourKey][model] || 0) + 1
|
|
364
365
|
pruneHours(now)
|
|
365
366
|
pruneRetentionOnStats(inMemoryStats, statsRetentionDays, now)
|
package/lib/telemetry.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* In-memory runtime session metrics for ClinePass requests in DeepSeek Harness.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { safeDict } from './safe-dict.js'
|
|
6
|
+
|
|
5
7
|
export const sessionStats = {
|
|
6
8
|
totalRequests: 0,
|
|
7
9
|
successfulRequests: 0,
|
|
@@ -15,7 +17,7 @@ export const sessionStats = {
|
|
|
15
17
|
lastLatencyMs: null,
|
|
16
18
|
lastRequestAt: null,
|
|
17
19
|
lastError: null,
|
|
18
|
-
byModel:
|
|
20
|
+
byModel: safeDict(),
|
|
19
21
|
lastSmoke: null,
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -49,8 +51,10 @@ export function recordSessionRequest({
|
|
|
49
51
|
sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
|
|
50
52
|
|
|
51
53
|
if (model) {
|
|
52
|
-
if (!sessionStats.byModel) sessionStats.byModel =
|
|
53
|
-
const m = sessionStats.byModel
|
|
54
|
+
if (!sessionStats.byModel) sessionStats.byModel = safeDict()
|
|
55
|
+
const m = Object.hasOwn(sessionStats.byModel, model)
|
|
56
|
+
? sessionStats.byModel[model]
|
|
57
|
+
: { requests: 0, promptTokens: 0, completionTokens: 0, reasoningTokens: 0 }
|
|
54
58
|
m.requests += 1
|
|
55
59
|
m.promptTokens += Number(promptTokens) || 0
|
|
56
60
|
m.completionTokens += Number(completionTokens) || 0
|
|
@@ -82,6 +86,6 @@ export function resetSessionStats() {
|
|
|
82
86
|
sessionStats.lastLatencyMs = null
|
|
83
87
|
sessionStats.lastRequestAt = null
|
|
84
88
|
sessionStats.lastError = null
|
|
85
|
-
sessionStats.byModel =
|
|
89
|
+
sessionStats.byModel = safeDict()
|
|
86
90
|
sessionStats.lastSmoke = null
|
|
87
91
|
}
|
package/lib/updater.js
CHANGED
|
@@ -14,16 +14,23 @@ import { fileURLToPath } from 'node:url'
|
|
|
14
14
|
const UPDATE_HEADER = 'x-dsh-plugin-update'
|
|
15
15
|
const UPDATE_TIMEOUT_MS = 10 * 60_000
|
|
16
16
|
const VERSION_CACHE_MS = 5 * 60_000
|
|
17
|
-
const
|
|
17
|
+
const SAFE_ENV_KEYS = new Set([
|
|
18
|
+
'PATH', 'HOME', 'USERPROFILE', 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT',
|
|
19
|
+
'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'SHELL', 'PWD',
|
|
20
|
+
'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)',
|
|
21
|
+
'PROGRAMW6432', 'SYSTEMDRIVE', 'NUMBER_OF_PROCESSORS', 'OS',
|
|
22
|
+
'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'USER', 'USERNAME',
|
|
23
|
+
'LOGNAME', 'HOSTNAME', 'DSH_HOME', 'DSH_PROFILE_DIR', 'NODE_EXTRA_CA_CERTS',
|
|
24
|
+
])
|
|
25
|
+
const SAFE_NPM_ENV_RE = /^npm_config_(?:registry|proxy|https_proxy|strict_ssl|userconfig|globalconfig|cache|prefix)$/i
|
|
18
26
|
let latestCache = undefined
|
|
19
27
|
|
|
20
28
|
export function sanitizeChildEnv(env = process.env) {
|
|
21
|
-
const out = {
|
|
22
|
-
for (const key of Object.
|
|
23
|
-
if (
|
|
24
|
-
delete out[key]
|
|
25
|
-
}
|
|
29
|
+
const out = {}
|
|
30
|
+
for (const [key, value] of Object.entries(env)) {
|
|
31
|
+
if (SAFE_ENV_KEYS.has(key) || SAFE_NPM_ENV_RE.test(key)) out[key] = value
|
|
26
32
|
}
|
|
33
|
+
out.NO_COLOR = '1'
|
|
27
34
|
return out
|
|
28
35
|
}
|
|
29
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.24",
|
|
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",
|