@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 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
- if (!isTrustedSettingsRequest(req)) {
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
- /** Reject cross-site writes while allowing local, LAN, and reverse-proxy UIs. */
57
- export function isTrustedSettingsRequest(request) {
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
- matched.push(found)
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
- matched.push({
244
- id: `cline-pass/${idPart}`,
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
 
@@ -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.23'
17
+ let packageVersion = '0.5.25'
18
18
  try {
19
- packageVersion = require('../package.json').version || '0.5.23'
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
  }
@@ -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 })
@@ -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 = {}
@@ -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 = {}
@@ -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.23",
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",