@goodandready/dsh-clinebot 0.5.23 → 0.5.25
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 +10 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/access.js +21 -4
- package/lib/config.js +4 -3
- package/lib/http.js +28 -2
- package/lib/models.js +26 -15
- package/lib/proxy-token.js +2 -2
- package/lib/routes/accounts.js +5 -5
- package/lib/routes/auth.js +2 -2
- package/lib/routes/models.js +5 -5
- package/lib/routes/settings.js +7 -7
- package/lib/updater.js +13 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.5.25] - 2026-10-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Subscription Plan Model Synchronization & Default Model Migration (Issue #224, GH #14)**: Updated canonical default model identifier and baseline catalog entry to `cline-pass/deepseek-v4.1-flash` (`DeepSeek V4.1 Flash`), matching upstream ClinePass plan changes. Added automatic silent migration for legacy `cline-pass/deepseek-v4-flash` in `OBSOLETE_MODEL_ID_MAP` and `publicConfig` (`defaultModel`, `disabledModels`, `enabledModels`), eliminating false-positive `Configured default model ... is not available in your active plan` warning banners. Updated `parsePlanIncludedModels` to resolve both legacy and modern plan model feature strings with model deduplication.
|
|
12
|
+
|
|
13
|
+
## [0.5.24] - 2026-10-13
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
- **Settings & Updater Authentication Boundary (Issue #222)**: Integrated `getConnection(ctx)` and delegated request verification to DSH core service `connection.requestRejection(request)`. When accessed across LAN bridges or reverse-proxies, unauthenticated requests lacking DSH session authentication are rejected with HTTP 401/403 fail-closed, preventing unauthorized credential modification (`/save-key`), account pool manipulation, model toggling, or telemetry resets from LAN clients.
|
|
17
|
+
|
|
8
18
|
## [0.5.23] - 2026-10-13
|
|
9
19
|
|
|
10
20
|
### Removed
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
Integrating ClinePass into DeepSeek Harness (DSH) natively poses key challenges:
|
|
44
44
|
1. **No `/v1/models` Discovery**: `GET /v1/models` on `api.cline.bot` returns `404 Not Found`. Dynamic discovery fails silently or leaves the provider with 0 models.
|
|
45
|
-
2. **Model Identifier Formats**: Models require the specific prefix `cline-pass/` (e.g. `cline-pass/deepseek-v4-flash`, `cline-pass/kimi-k3`).
|
|
45
|
+
2. **Model Identifier Formats**: Models require the specific prefix `cline-pass/` (e.g. `cline-pass/deepseek-v4.1-flash`, `cline-pass/kimi-k3`).
|
|
46
46
|
3. **Quota Tracking**: Rolling 5-hour and weekly limits need clear in-browser visualization.
|
|
47
47
|
4. **Credential Security**: Storing API keys directly in plain settings is insecure.
|
|
48
48
|
|
|
@@ -140,7 +140,7 @@ From any DSH chat session, type `/cline` to inspect quota, warning alerts, and s
|
|
|
140
140
|
### 🤖 ClinePass Status (ClinePass)
|
|
141
141
|
* Latency: ✅ 210 ms
|
|
142
142
|
* Active Key: CLINEBOT_API_KEY (credentials)
|
|
143
|
-
* Default Model: `cline-pass/deepseek-v4-flash`
|
|
143
|
+
* Default Model: `cline-pass/deepseek-v4.1-flash`
|
|
144
144
|
|
|
145
145
|
⏱ 5-Hour Window: [████░░░░░░] 42% (resets: 18:00)
|
|
146
146
|
📅 Weekly Window: [██████░░░░] 60% (resets: Sep 8)
|
|
@@ -186,7 +186,7 @@ dsh-clinebot:
|
|
|
186
186
|
| `enabled` | `boolean` | `true` | Enable or disable the ClineBot provider bridge in DSH |
|
|
187
187
|
| `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | ClinePass OpenAI-compatible base URL |
|
|
188
188
|
| `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | Environment variable / credentials key name |
|
|
189
|
-
| `defaultModel` | `string` | `"cline-pass/deepseek-v4-flash"` | Default selected model ID for chat and smoke tests |
|
|
189
|
+
| `defaultModel` | `string` | `"cline-pass/deepseek-v4.1-flash"` | Default selected model ID for chat and smoke tests |
|
|
190
190
|
| `disabledModels` | `array` | `[]` | List of model IDs hidden from the DSH chat picker (new subscription models are auto-enabled) |
|
|
191
191
|
| `customModels` | `array` | `[]` | User-defined gateway models (`[{ id, name, contextLength, maxTokens, category, isReasoning }]`) |
|
|
192
192
|
| `modelReasoningDefaults` | `object` | `{}` | Configured default reasoning effort per model (`low`, `medium`, `high`, `max`) |
|
package/README.ru.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
Интеграция ClinePass в DeepSeek Harness (DSH) напрямую сопряжена со следующими сложностями:
|
|
44
44
|
1. **Отсутствие эндпоинта `/v1/models`**: запрос `GET /v1/models` к `api.cline.bot` возвращает `404 Not Found`, из-за чего динамический поиск моделей в DSH падает или оставляет список пустым.
|
|
45
|
-
2. **Специфический формат идентификаторов**: моделям требуется обязательный префикс `cline-pass/` (например, `cline-pass/deepseek-v4-flash`, `cline-pass/kimi-k3`).
|
|
45
|
+
2. **Специфический формат идентификаторов**: моделям требуется обязательный префикс `cline-pass/` (например, `cline-pass/deepseek-v4.1-flash`, `cline-pass/kimi-k3`).
|
|
46
46
|
3. **Отслеживание лимитов**: 5-часовые и недельные скользящие окна расхода токенов требуют прозрачной визуализации в интерфейсе.
|
|
47
47
|
4. **Безопасность ключей**: хранение API-токенов в открытых конфигурациях небезопасно.
|
|
48
48
|
|
|
@@ -137,7 +137,7 @@ dsh plugin --profile web add @goodandready/dsh-clinebot
|
|
|
137
137
|
### 🤖 ClinePass Status (ClinePass)
|
|
138
138
|
* Пинг хоста: ✅ 210 мс
|
|
139
139
|
* Активный ключ: CLINEBOT_API_KEY (credentials)
|
|
140
|
-
* Модель по умолчанию: `cline-pass/deepseek-v4-flash`
|
|
140
|
+
* Модель по умолчанию: `cline-pass/deepseek-v4.1-flash`
|
|
141
141
|
|
|
142
142
|
⏱ 5-часовое окно: [████░░░░░░] 42% (сброс: 18:00)
|
|
143
143
|
📅 Недельное окно: [██████░░░░] 60% (сброс: 08.09)
|
|
@@ -183,7 +183,7 @@ dsh-clinebot:
|
|
|
183
183
|
| `enabled` | `boolean` | `true` | Включение моста провайдера ClineBot в DSH |
|
|
184
184
|
| `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | Базовый URL OpenAI-совместимого API ClinePass |
|
|
185
185
|
| `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | Имя переменной / ключа в хранилище credentials |
|
|
186
|
-
| `defaultModel` | `string` | `"cline-pass/deepseek-v4-flash"` | Модель, выбираемая по умолчанию для чата и smoke-тестов |
|
|
186
|
+
| `defaultModel` | `string` | `"cline-pass/deepseek-v4.1-flash"` | Модель, выбираемая по умолчанию для чата и smoke-тестов |
|
|
187
187
|
| `disabledModels` | `array` | `[]` | Список отключённых моделей (новые модели подписки включаются автоматически) |
|
|
188
188
|
| `customModels` | `array` | `[]` | Пользовательские модели шлюза (`[{ id, name, contextLength, maxTokens, category, isReasoning }]`) |
|
|
189
189
|
| `modelReasoningDefaults` | `object` | `{}` | Значения thinking effort по умолчанию для моделей (`low`, `medium`, `high`, `max`) |
|
package/README.zh.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
在将 ClinePass 接入 DeepSeek Harness (DSH) 时存在以下挑战:
|
|
44
44
|
1. **缺失 `/v1/models` 接口**:`api.cline.bot` 的 `GET /v1/models` 会直接返回 `404 Not Found`,导致动态模型同步失败或模型列表为空。
|
|
45
|
-
2. **专属模型前缀**:所有模型 ID 均需前缀 `cline-pass/`(如 `cline-pass/deepseek-v4-flash`, `cline-pass/kimi-k3`)。
|
|
45
|
+
2. **专属模型前缀**:所有模型 ID 均需前缀 `cline-pass/`(如 `cline-pass/deepseek-v4.1-flash`, `cline-pass/kimi-k3`)。
|
|
46
46
|
3. **用量额度监控**:5 小时滑动窗口与每周限额需要清晰直观的可视化进度监控。
|
|
47
47
|
4. **安全凭据隔离**:禁止在明文配置中直接填写密钥。
|
|
48
48
|
|
|
@@ -139,7 +139,7 @@ dsh plugin --profile web add @goodandready/dsh-clinebot
|
|
|
139
139
|
### 🤖 ClinePass Status (ClinePass)
|
|
140
140
|
* 响应延迟: ✅ 210 ms
|
|
141
141
|
* 活跃密钥: CLINEBOT_API_KEY (credentials)
|
|
142
|
-
* 默认模型: `cline-pass/deepseek-v4-flash`
|
|
142
|
+
* 默认模型: `cline-pass/deepseek-v4.1-flash`
|
|
143
143
|
|
|
144
144
|
⏱ 5 小时窗口: [████░░░░░░] 42% (重置时间: 18:00)
|
|
145
145
|
📅 每周窗口: [██████░░░░] 60% (重置时间: 09月08日)
|
|
@@ -185,7 +185,7 @@ dsh-clinebot:
|
|
|
185
185
|
| `enabled` | `boolean` | `true` | 是否在 DSH 中启用 ClineBot 桥接插件 |
|
|
186
186
|
| `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | ClinePass OpenAI 兼容接口地址 |
|
|
187
187
|
| `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | 凭据管理系统中的密钥名称 |
|
|
188
|
-
| `defaultModel` | `string` | `"cline-pass/deepseek-v4-flash"` | 默认选中的模型 ID(用于对话及探活) |
|
|
188
|
+
| `defaultModel` | `string` | `"cline-pass/deepseek-v4.1-flash"` | 默认选中的模型 ID(用于对话及探活) |
|
|
189
189
|
| `disabledModels` | `array` | `[]` | 在选择器中隐藏的模型 ID 列表(新订阅模型默认自动启用) |
|
|
190
190
|
| `customModels` | `array` | `[]` | 用户自定义网关模型 (`[{ id, name, contextLength, maxTokens, category, isReasoning }]`) |
|
|
191
191
|
| `modelReasoningDefaults` | `object` | `{}` | 各模型默认思考强度配置 (`low`, `medium`, `high`, `max`) |
|
package/lib/access.js
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import { isTrustedSettingsRequest, writeJson } from './http.js'
|
|
1
|
+
import { isTrustedSettingsRequest, getConnection, writeJson } from './http.js'
|
|
2
2
|
|
|
3
|
-
export { isTrustedSettingsRequest }
|
|
3
|
+
export { isTrustedSettingsRequest, getConnection }
|
|
4
4
|
|
|
5
|
-
export function assertTrustedSettingsRequest(req, res) {
|
|
6
|
-
|
|
5
|
+
export function assertTrustedSettingsRequest(req, res, ctx = null) {
|
|
6
|
+
const conn = getConnection(ctx) || req?.connectionService
|
|
7
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
8
|
+
let rejection
|
|
9
|
+
try {
|
|
10
|
+
rejection = conn.requestRejection(req)
|
|
11
|
+
} catch {
|
|
12
|
+
rejection = 403
|
|
13
|
+
}
|
|
14
|
+
if (rejection !== undefined) {
|
|
15
|
+
writeJson(res, rejection, {
|
|
16
|
+
ok: false,
|
|
17
|
+
error: rejection === 401 ? 'Authentication required' : 'Forbidden',
|
|
18
|
+
})
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
7
24
|
writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
8
25
|
return false
|
|
9
26
|
}
|
package/lib/config.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DEFAULT_MODEL_ID,
|
|
12
12
|
getAllModels,
|
|
13
13
|
getActiveModelIds,
|
|
14
|
+
migrateModelId,
|
|
14
15
|
} from './models.js'
|
|
15
16
|
|
|
16
17
|
// DSH settings.describe only publishes fields marked volatile. Without them the
|
|
@@ -137,9 +138,9 @@ export function publicConfig(cfg) {
|
|
|
137
138
|
const allModels = getAllModels(dynamic, modelContextOverrides, custom)
|
|
138
139
|
const allDefaultIds = allModels.map((m) => m.id)
|
|
139
140
|
|
|
140
|
-
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
|
|
141
|
+
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels.map(migrateModelId) : []
|
|
141
142
|
if (cfg?.disabledModels === undefined && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0) {
|
|
142
|
-
const enabledSet = new Set(cfg.enabledModels)
|
|
143
|
+
const enabledSet = new Set(cfg.enabledModels.map(migrateModelId))
|
|
143
144
|
disabledList = allDefaultIds.filter((id) => !enabledSet.has(id))
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -148,7 +149,7 @@ export function publicConfig(cfg) {
|
|
|
148
149
|
activeIds = [allDefaultIds[0]]
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
let defaultModel = cfg?.defaultModel || DEFAULT_MODEL_ID
|
|
152
|
+
let defaultModel = migrateModelId(cfg?.defaultModel || DEFAULT_MODEL_ID)
|
|
152
153
|
let defaultModelWarning = null
|
|
153
154
|
|
|
154
155
|
if (allDefaultIds.length > 0 && !allDefaultIds.includes(defaultModel)) {
|
package/lib/http.js
CHANGED
|
@@ -53,8 +53,34 @@ export function isLoopbackAddress(value) {
|
|
|
53
53
|
|| address.startsWith('::ffff:127.')
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
export function getConnection(ctx) {
|
|
57
|
+
try {
|
|
58
|
+
const fromCtx = ctx?.reflect?.get
|
|
59
|
+
? (ctx.reflect.get('connection') || ctx?.connection)
|
|
60
|
+
: (ctx?.get ? (ctx.get('connection') || ctx?.connection) : Reflect.get(ctx || {}, 'connection'))
|
|
61
|
+
if (fromCtx) return fromCtx
|
|
62
|
+
if (ctx?.root) {
|
|
63
|
+
return getConnection(ctx.root)
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Reject unauthenticated LAN/remote writes while allowing admitted DSH and local requests (#222). */
|
|
72
|
+
export function isTrustedSettingsRequest(request, ctx = null) {
|
|
73
|
+
// 1. Core DSH connection auth gate (#222)
|
|
74
|
+
const conn = getConnection(ctx) || request?.connectionService
|
|
75
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
76
|
+
let rejection
|
|
77
|
+
try {
|
|
78
|
+
rejection = conn.requestRejection(request)
|
|
79
|
+
} catch {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
if (rejection !== undefined) return false
|
|
83
|
+
}
|
|
58
84
|
const secFetchSite = header(request, 'sec-fetch-site')
|
|
59
85
|
if (secFetchSite === 'cross-site') {
|
|
60
86
|
return false
|
package/lib/models.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
export const PROVIDER_ID = 'clinebot'
|
|
14
14
|
|
|
15
15
|
export const OBSOLETE_MODEL_ID_MAP = Object.freeze({
|
|
16
|
+
'cline-pass/deepseek-v4-flash': 'cline-pass/deepseek-v4.1-flash',
|
|
16
17
|
'cline-pass/deepseek-v41-flash': 'cline-pass/deepseek-v4.1-flash',
|
|
17
18
|
'cline-pass/qwen38-max': 'cline-pass/qwen3.8-max',
|
|
18
19
|
'cline-pass/glm-53': 'cline-pass/glm-5.3',
|
|
@@ -20,7 +21,7 @@ export const OBSOLETE_MODEL_ID_MAP = Object.freeze({
|
|
|
20
21
|
'cline-pass/muse-spark-13-contributor': 'cline-pass/muse-spark-1.3-contributor',
|
|
21
22
|
})
|
|
22
23
|
|
|
23
|
-
function migrateModelId(id) {
|
|
24
|
+
export function migrateModelId(id) {
|
|
24
25
|
if (typeof id !== 'string') return id
|
|
25
26
|
return OBSOLETE_MODEL_ID_MAP[id] || id
|
|
26
27
|
}
|
|
@@ -35,7 +36,7 @@ export function migrateModelEntry(model) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
export const PROVIDER_DISPLAY_NAME = 'ClineBot (ClinePass)'
|
|
38
|
-
export const DEFAULT_MODEL_ID = 'cline-pass/deepseek-v4-flash'
|
|
39
|
+
export const DEFAULT_MODEL_ID = 'cline-pass/deepseek-v4.1-flash'
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Authentic original model specifications from original creators:
|
|
@@ -104,8 +105,8 @@ function createCuratedModel({
|
|
|
104
105
|
|
|
105
106
|
export const CLINE_MODELS = Object.freeze([
|
|
106
107
|
createCuratedModel({
|
|
107
|
-
id: 'cline-pass/deepseek-v4-flash',
|
|
108
|
-
name: 'DeepSeek V4 Flash',
|
|
108
|
+
id: 'cline-pass/deepseek-v4.1-flash',
|
|
109
|
+
name: 'DeepSeek V4.1 Flash',
|
|
109
110
|
description: 'High-speed reasoning & code completion model optimized for agentic loops.',
|
|
110
111
|
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
|
|
111
112
|
reasoningEfforts: ['low', 'medium', 'high'],
|
|
@@ -216,32 +217,38 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
216
217
|
.replace(/[\s._-]+/g, '')
|
|
217
218
|
|
|
218
219
|
const matched = []
|
|
220
|
+
const seenIds = new Set()
|
|
219
221
|
for (const name of parts) {
|
|
220
222
|
const targetNorm = normalize(name)
|
|
223
|
+
const idPart = name
|
|
224
|
+
.toLowerCase()
|
|
225
|
+
.trim()
|
|
226
|
+
.replace(/^cline-pass\//, '')
|
|
227
|
+
.replace(/qwen\s+([0-9])/i, 'qwen$1')
|
|
228
|
+
.replace(/glm\s+([0-9])/i, 'glm-$1')
|
|
229
|
+
.replace(/[\s_]+/g, '-')
|
|
230
|
+
.replace(/-+/g, '-')
|
|
231
|
+
const candidateId = migrateModelId(`cline-pass/${idPart}`)
|
|
221
232
|
const found = CLINE_MODELS.find(
|
|
222
233
|
(m) =>
|
|
234
|
+
m.id === candidateId ||
|
|
223
235
|
normalize(m.name) === targetNorm ||
|
|
224
236
|
normalize(m.id.replace(/^cline-pass\//, '')) === targetNorm
|
|
225
237
|
)
|
|
226
238
|
if (found) {
|
|
227
|
-
|
|
239
|
+
if (!seenIds.has(found.id)) {
|
|
240
|
+
seenIds.add(found.id)
|
|
241
|
+
matched.push(found)
|
|
242
|
+
}
|
|
228
243
|
} else {
|
|
229
244
|
// Dynamic fallback for newly introduced models mentioned in plan
|
|
230
|
-
const idPart = name
|
|
231
|
-
.toLowerCase()
|
|
232
|
-
.trim()
|
|
233
|
-
.replace(/^cline-pass\//, '')
|
|
234
|
-
.replace(/qwen\s+([0-9])/i, 'qwen$1')
|
|
235
|
-
.replace(/glm\s+([0-9])/i, 'glm-$1')
|
|
236
|
-
.replace(/[\s_]+/g, '-')
|
|
237
|
-
.replace(/-+/g, '-')
|
|
238
245
|
const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
|
|
239
246
|
const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
|
|
240
247
|
const origCtx = getOriginalModelContext(name)
|
|
241
248
|
const origMax = getOriginalModelMaxTokens(name)
|
|
242
249
|
const origProv = getOriginalModelProvider(name)
|
|
243
|
-
|
|
244
|
-
id:
|
|
250
|
+
const dynamicModel = migrateModelEntry({
|
|
251
|
+
id: candidateId,
|
|
245
252
|
name,
|
|
246
253
|
description: `Official ClinePass subscription model: ${name}`,
|
|
247
254
|
clineContextLength: 200000,
|
|
@@ -258,6 +265,10 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
258
265
|
isCustom: false,
|
|
259
266
|
...(hasReasoning ? { reasoningEfforts: ['low', 'medium', 'high', 'max'] } : {}),
|
|
260
267
|
})
|
|
268
|
+
if (!seenIds.has(dynamicModel.id)) {
|
|
269
|
+
seenIds.add(dynamicModel.id)
|
|
270
|
+
matched.push(dynamicModel)
|
|
271
|
+
}
|
|
261
272
|
}
|
|
262
273
|
}
|
|
263
274
|
|
package/lib/proxy-token.js
CHANGED
|
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
|
|
|
14
14
|
let cachedToken = null
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url)
|
|
17
|
-
let packageVersion = '0.5.
|
|
17
|
+
let packageVersion = '0.5.25'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
19
|
+
packageVersion = require('../package.json').version || '0.5.25'
|
|
20
20
|
} catch (err) {
|
|
21
21
|
/* fallback to default version if manifest is unreadable */
|
|
22
22
|
}
|
package/lib/routes/accounts.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
23
23
|
return handleDeleteAccount(req, res)
|
|
24
24
|
}
|
|
25
25
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST or DELETE only' })
|
|
26
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
26
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
27
27
|
const settingsApi = getSettingsApi()
|
|
28
28
|
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
29
29
|
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
@@ -114,7 +114,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
114
114
|
}), 'dsh-clinebot: /accounts')
|
|
115
115
|
|
|
116
116
|
async function handleDeleteAccount(req, res, targetEnvFromUrl = '') {
|
|
117
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
117
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
118
118
|
const settingsApi = getSettingsApi()
|
|
119
119
|
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
120
120
|
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
@@ -271,7 +271,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
271
271
|
path: '/dsh-clinebot/accounts/active',
|
|
272
272
|
handler: async (req, res) => {
|
|
273
273
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
274
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
274
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
275
275
|
try {
|
|
276
276
|
const bodyBuf = await readBody(req)
|
|
277
277
|
let body = {}
|
|
@@ -307,7 +307,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
307
307
|
path: '/dsh-clinebot/register',
|
|
308
308
|
handler: async (req, res) => {
|
|
309
309
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
310
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
310
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
311
311
|
try {
|
|
312
312
|
const bodyBuf = await readBody(req)
|
|
313
313
|
let body = {}
|
|
@@ -327,7 +327,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
327
327
|
path: '/dsh-clinebot/unregister',
|
|
328
328
|
handler: async (req, res) => {
|
|
329
329
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
330
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
330
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
331
331
|
try {
|
|
332
332
|
await removePiAiProvider(ctx)
|
|
333
333
|
writeJson(res, 200, { ok: true })
|
package/lib/routes/auth.js
CHANGED
|
@@ -16,7 +16,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
|
|
|
16
16
|
kind: 'exact',
|
|
17
17
|
path: '/dsh-clinebot/key/verify',
|
|
18
18
|
handler: async (req, res) => {
|
|
19
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
19
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
20
20
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
21
21
|
try {
|
|
22
22
|
const bodyBuf = await readBody(req)
|
|
@@ -96,7 +96,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
|
|
|
96
96
|
path: '/dsh-clinebot/smoke',
|
|
97
97
|
handler: async (req, res) => {
|
|
98
98
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
99
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
99
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
100
100
|
try {
|
|
101
101
|
const bodyBuf = await readBody(req)
|
|
102
102
|
let body = {}
|
package/lib/routes/models.js
CHANGED
|
@@ -12,7 +12,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
12
12
|
kind: 'exact',
|
|
13
13
|
path: '/dsh-clinebot/models/sync',
|
|
14
14
|
handler: async (req, res) => {
|
|
15
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
15
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
16
16
|
if (req.method !== 'POST') {
|
|
17
17
|
return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
18
18
|
}
|
|
@@ -76,7 +76,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
76
76
|
path: '/dsh-clinebot/models/toggle',
|
|
77
77
|
handler: async (req, res) => {
|
|
78
78
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
79
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
79
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
80
80
|
try {
|
|
81
81
|
const bodyBuf = await readBody(req)
|
|
82
82
|
let body = {}
|
|
@@ -113,7 +113,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
113
113
|
path: '/dsh-clinebot/models/context',
|
|
114
114
|
handler: async (req, res) => {
|
|
115
115
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
116
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
116
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
117
117
|
try {
|
|
118
118
|
const bodyBuf = await readBody(req)
|
|
119
119
|
let body = {}
|
|
@@ -223,7 +223,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
223
223
|
path: '/dsh-clinebot/usage',
|
|
224
224
|
handler: async (req, res) => {
|
|
225
225
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
226
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
226
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
227
227
|
try {
|
|
228
228
|
const pub = publicConfig(live())
|
|
229
229
|
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
@@ -247,7 +247,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
247
247
|
path: '/dsh-clinebot/models/probe',
|
|
248
248
|
handler: async (req, res) => {
|
|
249
249
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
250
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
250
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
251
251
|
try {
|
|
252
252
|
const bodyBuf = await readBody(req)
|
|
253
253
|
let body = {}
|
package/lib/routes/settings.js
CHANGED
|
@@ -12,7 +12,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
12
12
|
path: '/dsh-clinebot/status',
|
|
13
13
|
handler: async (req, res) => {
|
|
14
14
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
15
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
15
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
16
16
|
try {
|
|
17
17
|
const st = await buildStatus(ctx, live())
|
|
18
18
|
writeJson(res, 200, st)
|
|
@@ -28,13 +28,13 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
28
28
|
path: '/dsh-clinebot/config',
|
|
29
29
|
handler: async (req, res) => {
|
|
30
30
|
if (req.method === 'GET') {
|
|
31
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
31
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
32
32
|
return writeJson(res, 200, { ok: true, config: publicConfig(live()) })
|
|
33
33
|
}
|
|
34
34
|
if (req.method !== 'PUT') {
|
|
35
35
|
return writeJson(res, 405, { ok: false, error: 'GET or PUT' })
|
|
36
36
|
}
|
|
37
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
37
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
38
38
|
const settingsApi = getSettingsApi()
|
|
39
39
|
if (!settingsApi) {
|
|
40
40
|
return writeJson(res, 503, { ok: false, error: 'settings not ready' })
|
|
@@ -116,7 +116,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
116
116
|
path: '/dsh-clinebot/save-key',
|
|
117
117
|
handler: async (req, res) => {
|
|
118
118
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
119
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
119
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
120
120
|
try {
|
|
121
121
|
const bodyBuf = await readBody(req)
|
|
122
122
|
let body = {}
|
|
@@ -186,7 +186,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
186
186
|
path: '/dsh-clinebot/stats',
|
|
187
187
|
handler: async (req, res) => {
|
|
188
188
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
189
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
189
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
190
190
|
try {
|
|
191
191
|
const stats = getStatsSummary()
|
|
192
192
|
writeJson(res, 200, { ok: true, stats })
|
|
@@ -202,7 +202,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
202
202
|
path: '/dsh-clinebot/stats/reset',
|
|
203
203
|
handler: async (req, res) => {
|
|
204
204
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
205
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
205
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
206
206
|
try {
|
|
207
207
|
const pub = publicConfig(live())
|
|
208
208
|
const stats = resetStats(pub.statsPath)
|
|
@@ -219,7 +219,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
219
219
|
path: '/dsh-clinebot/stats/export',
|
|
220
220
|
handler: async (req, res) => {
|
|
221
221
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
222
|
-
if (!assertTrustedSettingsRequest(req, res)) return
|
|
222
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
223
223
|
try {
|
|
224
224
|
const csv = exportStatsAsCsv()
|
|
225
225
|
res.statusCode = 200
|
package/lib/updater.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getConnection } from './http.js'
|
|
1
2
|
/**
|
|
2
3
|
* Host-side one-click updater for @goodandready/dsh-clinebot.
|
|
3
4
|
* Conforms to the DSH authoring reference implementation.
|
|
@@ -27,8 +28,18 @@ function isLoopback(value) {
|
|
|
27
28
|
|| address?.startsWith('::ffff:127.') === true
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export function isTrustedUpdateRequest(request) {
|
|
31
|
+
export function isTrustedUpdateRequest(request, targetCtx = null) {
|
|
31
32
|
if (header(request, UPDATE_HEADER) !== '1') return false
|
|
33
|
+
const conn = getConnection(targetCtx) || request?.connectionService
|
|
34
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
35
|
+
let rejection
|
|
36
|
+
try {
|
|
37
|
+
rejection = conn.requestRejection(request)
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
if (rejection !== undefined) return false
|
|
42
|
+
}
|
|
32
43
|
if (!isLoopback(request?.socket?.remoteAddress)) return false
|
|
33
44
|
const site = header(request, 'sec-fetch-site')
|
|
34
45
|
if (site !== undefined && site !== 'same-origin') return false
|
|
@@ -388,7 +399,7 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
388
399
|
response.end()
|
|
389
400
|
return
|
|
390
401
|
}
|
|
391
|
-
if (!isTrustedUpdateRequest(request)) {
|
|
402
|
+
if (!isTrustedUpdateRequest(request, ctx)) {
|
|
392
403
|
writeResponseJson(response, 403, { ok: false, error: 'Rejected non-local or cross-origin update request.' })
|
|
393
404
|
return
|
|
394
405
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.25",
|
|
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",
|