@goodandready/dsh-clinebot 0.3.15 → 0.3.17

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,28 @@ 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.3.17] - 2026-09-21
9
+
10
+ ### Fixed
11
+ - **Cache Resilience (#50)**: Fixed permanent `isRevalidating` flag lock in `usageCache` and `probeCache` upon background SWR revalidation failures. The flag is now guaranteed to reset in `finally` and `catch` blocks.
12
+ - **Immediate Abort Handling (#52)**: Eliminated artificial 250ms delay and futile retry upon `AbortError` or `signal.aborted` in `fetchUsageLimits`.
13
+ - **Lifecycle Timer Cleanup (#53)**: Wrapped `autoDiscover` timer in `ctx.effect` with cleanup handler to avoid orphaned background callbacks upon plugin context unload.
14
+ - **UI Version Hardcode Removal (#56)**: Replaced hardcoded legacy `'0.3.12'` string in initial React state with dynamic retrieval from `/update` endpoint and conditional badge rendering.
15
+
16
+ ### Performance
17
+ - **Single Account Pool Resolution (#51)**: Eliminated duplicate sequential IPC queries to DSH Credentials by passing the pre-resolved account pool into `resolveActiveAccountKey` within `buildStatus`.
18
+ - **Bounded Cache with Auto-Eviction (#57)**: Enforced `MAX_CACHE_ENTRIES = 50` and automatic eviction of expired entries in `usageCache` and `probeCache` to prevent long-term memory leaks.
19
+
20
+ ### Refactored
21
+ - **Active Route Guard (#54)**: Converted `assertTrustedSettingsRequest` into an active route guard writing `403 Forbidden` and applied it across all write endpoints in `lib/routes/`.
22
+ - **Dead Export Wiring (#55)**: Wired `getDefaultModelIds` helper in `lib/config.js` to compute `allDefaultIds`, clearing preflight dead-export warnings.
23
+
24
+ ## [0.3.16] - 2026-09-20
25
+
26
+ ### Fixed
27
+ - **Strict Active Subscription Plan Model Filtering (#48)**: Correctly parsed `plan.features.included` array from `GET /users/me/plan` to discover exactly the 11 active ClinePass subscription models. Removed non-subscription models from the default `CLINE_MODELS` list (eliminating out-of-plan failures and preventing 400+ models from cluttering the DSH picker).
28
+ - **Array Parsing in `parsePlanIncludedModels`**: Added support for both string and array inputs (extracting feature entries matching `/includes\s+/i`).
29
+
8
30
  ## [0.3.15] - 2026-09-19
9
31
 
10
32
  ### Fixed
package/lib/access.js CHANGED
@@ -1,9 +1,10 @@
1
- import { isTrustedSettingsRequest } from './http.js'
1
+ import { isTrustedSettingsRequest, writeJson } from './http.js'
2
2
 
3
3
  export { isTrustedSettingsRequest }
4
4
 
5
5
  export function assertTrustedSettingsRequest(req, res) {
6
6
  if (!isTrustedSettingsRequest(req)) {
7
+ writeJson(res, 403, { ok: false, error: 'Forbidden' })
7
8
  return false
8
9
  }
9
10
  return true
package/lib/client.js CHANGED
@@ -402,9 +402,11 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
402
402
  React.createElement(
403
403
  'div',
404
404
  { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' } },
405
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
406
- `v${updateState.currentVersion}`
407
- ),
405
+ updateState.currentVersion
406
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
407
+ `v${updateState.currentVersion}`
408
+ )
409
+ : null,
408
410
  updateState.checking
409
411
  ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
410
412
  t('update.checking')
@@ -882,7 +884,7 @@ function SettingsPage(props) {
882
884
  const [updateState, setUpdateState] = React.useState({
883
885
  checking: false,
884
886
  updating: false,
885
- currentVersion: '0.3.12',
887
+ currentVersion: '',
886
888
  latestVersion: '',
887
889
  updateAvailable: false,
888
890
  canAutoUpdate: true,
@@ -132,9 +132,26 @@ function abortAfter(ms) {
132
132
  }
133
133
 
134
134
  // In-memory caches for quota queries and host health probes
135
+ export const MAX_CACHE_ENTRIES = 50
135
136
  export const usageCache = new Map()
136
137
  export const probeCache = new Map()
137
138
 
139
+ export function setBoundedCache(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
140
+ if (map.size >= maxEntries) {
141
+ const now = Date.now()
142
+ for (const [k, v] of map.entries()) {
143
+ if (v.expiresAt && v.expiresAt < now) {
144
+ map.delete(k)
145
+ }
146
+ }
147
+ if (map.size >= maxEntries) {
148
+ const oldestKey = map.keys().next().value
149
+ if (oldestKey) map.delete(oldestKey)
150
+ }
151
+ }
152
+ map.set(key, value)
153
+ }
154
+
138
155
  export function clearProbeCache() {
139
156
  probeCache.clear()
140
157
  }
@@ -212,8 +229,10 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
212
229
  keepalive: true,
213
230
  })
214
231
  } catch (netErr) {
232
+ if (signal?.aborted || netErr?.name === 'AbortError') throw netErr
215
233
  // Quick 1-retry fallback on transient network drop
216
234
  await new Promise((r) => setTimeout(r, 250))
235
+ if (signal?.aborted || netErr?.name === 'AbortError') throw netErr
217
236
  limitsRes = await fetchImpl(`${base}/users/me/plan/usage-limits`, {
218
237
  method: 'GET',
219
238
  headers,
@@ -277,12 +296,13 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
277
296
 
278
297
  if (planRes?.ok) {
279
298
  const planData = await planRes.json().catch(() => ({}))
280
- const plan = planData?.data || planData
281
- if (plan?.title || plan?.name) {
282
- planDisplayName = `${plan.title || plan.name} ($${((plan.priceInCents || 999) / 100).toFixed(2)}/mo)`
299
+ const plan = planData?.data?.plan || planData?.data || planData?.plan || planData
300
+ if (plan?.displayName || plan?.title || plan?.name) {
301
+ planDisplayName = `${plan.displayName || plan.title || plan.name} ($${((plan.pricePerSeatCents || plan.priceInCents || 999) / 100).toFixed(2)}/mo)`
283
302
  }
284
- if (Array.isArray(plan?.includedModels)) {
285
- dynamicModels = parsePlanIncludedModels(plan.includedModels)
303
+ const featuresIncluded = plan?.features?.included || plan?.includedModels
304
+ if (featuresIncluded) {
305
+ dynamicModels = parsePlanIncludedModels(featuresIncluded)
286
306
  }
287
307
  }
288
308
  } catch {
@@ -298,7 +318,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
298
318
  checkedAt: Date.now(),
299
319
  }
300
320
 
301
- usageCache.set(cacheKey, { data: result, expiresAt: Date.now() + ttlMs, isRevalidating: false })
321
+ setBoundedCache(usageCache, cacheKey, { data: result, expiresAt: Date.now() + ttlMs, isRevalidating: false })
302
322
  return result
303
323
  } catch (err) {
304
324
  const outcome = {
@@ -307,10 +327,15 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
307
327
  checkedAt: Date.now(),
308
328
  }
309
329
  if (!usageCache.has(cacheKey)) {
310
- usageCache.set(cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
330
+ setBoundedCache(usageCache, cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
331
+ } else {
332
+ const existing = usageCache.get(cacheKey)
333
+ if (existing) existing.isRevalidating = false
311
334
  }
312
335
  return outcome
313
336
  } finally {
337
+ const existing = usageCache.get(cacheKey)
338
+ if (existing) existing.isRevalidating = false
314
339
  cancel()
315
340
  }
316
341
  }
@@ -361,7 +386,7 @@ export async function probeHealth(baseUrl, {
361
386
  error: reachable ? null : `HTTP status ${res.status}`,
362
387
  checkedAt: Date.now(),
363
388
  }
364
- probeCache.set(cacheKey, { data: outcome, expiresAt: Date.now() + ttlMs, isRevalidating: false })
389
+ setBoundedCache(probeCache, cacheKey, { data: outcome, expiresAt: Date.now() + ttlMs, isRevalidating: false })
365
390
  return outcome
366
391
  } catch (err) {
367
392
  const latencyMs = Date.now() - start
@@ -371,9 +396,16 @@ export async function probeHealth(baseUrl, {
371
396
  error: String(err?.message || err),
372
397
  checkedAt: Date.now(),
373
398
  }
374
- probeCache.set(cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
399
+ if (!probeCache.has(cacheKey)) {
400
+ setBoundedCache(probeCache, cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
401
+ } else {
402
+ const existing = probeCache.get(cacheKey)
403
+ if (existing) existing.isRevalidating = false
404
+ }
375
405
  return outcome
376
406
  } finally {
407
+ const existing = probeCache.get(cacheKey)
408
+ if (existing) existing.isRevalidating = false
377
409
  cancel()
378
410
  }
379
411
  }
package/lib/config.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  DEFAULT_MODEL_ID,
11
11
  getAllModels,
12
+ getDefaultModelIds,
12
13
  getActiveModelIds,
13
14
  } from './models.js'
14
15
 
@@ -56,8 +57,7 @@ export const Config = z.object({
56
57
 
57
58
  export function publicConfig(cfg) {
58
59
  const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
59
- const allModels = getAllModels(dynamic)
60
- const allDefaultIds = allModels.map((m) => m.id)
60
+ const allDefaultIds = getDefaultModelIds(dynamic)
61
61
 
62
62
  let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
63
63
  if (!Array.isArray(cfg?.disabledModels) || (cfg.disabledModels.length === 0 && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0)) {
package/lib/index.js CHANGED
@@ -83,7 +83,14 @@ export function apply(ctx, config) {
83
83
  }
84
84
 
85
85
  syncProviderState(live())
86
- setTimeout(triggerAutoDiscover, 500)
86
+ if (typeof ctx.effect === 'function') {
87
+ ctx.effect(() => {
88
+ const timer = setTimeout(triggerAutoDiscover, 500)
89
+ return () => clearTimeout(timer)
90
+ }, 'dsh-clinebot: auto-discover')
91
+ } else {
92
+ setTimeout(triggerAutoDiscover, 500)
93
+ }
87
94
 
88
95
  if (ctx.webServer?.register) {
89
96
  const unregisterUpdater = registerPluginUpdater(ctx, {
package/lib/models.js CHANGED
@@ -136,83 +136,27 @@ export const CLINE_MODELS = Object.freeze([
136
136
  recommended: false,
137
137
  isCustom: false,
138
138
  },
139
- {
140
- id: 'cline-pass/claude-3-7-sonnet',
141
- name: 'Claude 3.7 Sonnet',
142
- description: 'Hybrid reasoning and standard generation model with high coding proficiency.',
143
- contextLength: 200000,
144
- maxTokens: 8192,
145
- input: ['text', 'image'],
146
- category: 'coding',
147
- recommended: true,
148
- isCustom: false,
149
- reasoningEfforts: ['low', 'medium', 'high'],
150
- },
151
- {
152
- id: 'cline-pass/gpt-4.5-preview',
153
- name: 'GPT-4.5 Preview',
154
- description: 'Advanced flagship frontier model with deep world knowledge and intuition.',
155
- contextLength: 128000,
156
- maxTokens: 16384,
157
- input: ['text', 'image'],
158
- category: 'general',
159
- recommended: true,
160
- isCustom: false,
161
- },
162
- {
163
- id: 'cline-pass/o3-mini',
164
- name: 'o3-mini',
165
- description: 'Fast, cost-effective reasoning model specialized for STEM and coding.',
166
- contextLength: 200000,
167
- maxTokens: 65536,
168
- input: ['text'],
169
- category: 'reasoning',
170
- recommended: true,
171
- isCustom: false,
172
- reasoningEfforts: ['low', 'medium', 'high'],
173
- },
174
- {
175
- id: 'cline-pass/gemini-2.5-pro',
176
- name: 'Gemini 2.5 Pro',
177
- description: 'State-of-the-art multimodal reasoning model with extended context.',
178
- contextLength: 1000000,
179
- maxTokens: 8192,
180
- input: ['text', 'image'],
181
- category: 'multimodal',
182
- recommended: true,
183
- isCustom: false,
184
- reasoningEfforts: ['low', 'medium', 'high'],
185
- },
186
- {
187
- id: 'cline-pass/gemini-2.5-flash',
188
- name: 'Gemini 2.5 Flash',
189
- description: 'Ultra-fast multimodal model optimized for real-time agent workflows.',
190
- contextLength: 1000000,
191
- maxTokens: 8192,
192
- input: ['text', 'image'],
193
- category: 'general',
194
- recommended: false,
195
- isCustom: false,
196
- },
197
- {
198
- id: 'cline-pass/qwen-2.5-coder-32b',
199
- name: 'Qwen 2.5 Coder 32B',
200
- description: 'Open-weights powerhouse for code generation, refactoring and bug fixing.',
201
- contextLength: 131072,
202
- maxTokens: 8192,
203
- input: ['text'],
204
- category: 'coding',
205
- recommended: false,
206
- isCustom: false,
207
- },
208
139
  ])
209
140
 
210
141
  /**
211
142
  * Parse human-readable included models string from ClinePass plan features.
212
143
  * Example: "Includes Kimi K3, GLM 5.2, Kimi K2.6, Kimi K2.7 Code, Mimo v2.5, Mimo v2.5 Pro, Minimax M3, Qwen3.7 Plus, Qwen3.7 Max, DeepSeek V4 Pro, and DeepSeek V4 Flash"
213
144
  */
214
- export function parsePlanIncludedModels(includedText) {
215
- if (!includedText || typeof includedText !== 'string') return []
145
+ export function parsePlanIncludedModels(includedInput) {
146
+ if (!includedInput) return []
147
+ let includedText = ''
148
+ if (Array.isArray(includedInput)) {
149
+ const foundStr = includedInput.find((item) => typeof item === 'string' && /includes\s+/i.test(item))
150
+ if (foundStr) {
151
+ includedText = foundStr
152
+ } else {
153
+ includedText = includedInput.filter((x) => typeof x === 'string').join(', ')
154
+ }
155
+ } else if (typeof includedInput === 'string') {
156
+ includedText = includedInput
157
+ } else {
158
+ return []
159
+ }
216
160
  const clean = includedText
217
161
  .replace(/^includes\s+/i, '')
218
162
  .replace(/\band\b/gi, ',')
@@ -37,9 +37,9 @@ export async function checkRegisteredInPiAi(ctx) {
37
37
  }
38
38
  }
39
39
 
40
- export async function resolveActiveAccountKey(ctx, cfg) {
41
- const pool = await resolveAccountPool(ctx, cfg)
42
- const configured = pool.filter((acc) => acc.present && acc.value)
40
+ export async function resolveActiveAccountKey(ctx, cfg, pool = null) {
41
+ const accountPool = pool || await resolveAccountPool(ctx, cfg)
42
+ const configured = accountPool.filter((acc) => acc.present && acc.value)
43
43
  if (!configured.length) {
44
44
  return { envName: publicConfig(cfg).apiKeyEnv, value: '', source: 'none', id: 'default' }
45
45
  }
@@ -62,7 +62,7 @@ export async function buildStatus(ctx, cfg) {
62
62
  probeHealth(pub.baseUrl, { timeoutMs: probeTimeout }),
63
63
  ])
64
64
 
65
- const activeAcc = await resolveActiveAccountKey(ctx, cfg)
65
+ const activeAcc = await resolveActiveAccountKey(ctx, cfg, pool)
66
66
  const allModels = getAllModels(pub.dynamicModels)
67
67
 
68
68
  let usage = null
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, Config } from '../config.js'
4
4
  import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
5
5
  import { clearUsageCache, clearProbeCache } from '../cline-client.js'
@@ -11,13 +11,11 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
11
11
  path: '/dsh-clinebot/accounts/active',
12
12
  handler: async (req, res) => {
13
13
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
14
- if (!isTrustedSettingsRequest(req)) {
15
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
16
- }
14
+ if (!assertTrustedSettingsRequest(req, res)) return
17
15
  try {
18
16
  const bodyBuf = await readBody(req)
19
17
  let body = {}
20
- try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
18
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
21
19
  const account = String(body.account || '').trim()
22
20
 
23
21
  clearUsageCache()
@@ -43,13 +41,11 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
43
41
  path: '/dsh-clinebot/register',
44
42
  handler: async (req, res) => {
45
43
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
46
- if (!isTrustedSettingsRequest(req)) {
47
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
48
- }
44
+ if (!assertTrustedSettingsRequest(req, res)) return
49
45
  try {
50
46
  const bodyBuf = await readBody(req)
51
47
  let body = {}
52
- try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
48
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
53
49
  const activeModels = body.models || publicConfig(live()).enabledModels
54
50
  const result = await upsertPiAiProvider(ctx, live(), activeModels)
55
51
  writeJson(res, 200, { ok: true, provider: result })
@@ -65,9 +61,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
65
61
  path: '/dsh-clinebot/unregister',
66
62
  handler: async (req, res) => {
67
63
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
68
- if (!isTrustedSettingsRequest(req)) {
69
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
70
- }
64
+ if (!assertTrustedSettingsRequest(req, res)) return
71
65
  try {
72
66
  await removePiAiProvider(ctx)
73
67
  writeJson(res, 200, { ok: true })
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig } from '../config.js'
4
4
  import { resolveActiveAccountKey } from '../provider-sync.js'
5
5
  import { smokeChat, recordSessionRequest, rotateToNextAccount, DEFAULT_MODEL_ID } from '../cline-client.js'
@@ -13,9 +13,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
13
13
  path: '/dsh-clinebot/auth/begin',
14
14
  handler: async (req, res) => {
15
15
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
16
- if (!isTrustedSettingsRequest(req)) {
17
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
18
- }
16
+ if (!assertTrustedSettingsRequest(req, res)) return
19
17
  try {
20
18
  const authUrl = 'https://app.cline.bot'
21
19
  authSession = {
@@ -54,13 +52,11 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
54
52
  path: '/dsh-clinebot/smoke',
55
53
  handler: async (req, res) => {
56
54
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
57
- if (!isTrustedSettingsRequest(req)) {
58
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
59
- }
55
+ if (!assertTrustedSettingsRequest(req, res)) return
60
56
  try {
61
57
  const bodyBuf = await readBody(req)
62
58
  let body = {}
63
- try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
59
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
64
60
 
65
61
  const pub = publicConfig(live())
66
62
  const activeKey = await resolveActiveAccountKey(ctx, live())
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, Config } from '../config.js'
4
4
  import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
5
5
  import { getAllModels, saveModelsDiskCache } from '../models.js'
@@ -11,9 +11,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
11
11
  kind: 'exact',
12
12
  path: '/dsh-clinebot/models/sync',
13
13
  handler: async (req, res) => {
14
- if (!isTrustedSettingsRequest(req)) {
15
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
16
- }
14
+ if (!assertTrustedSettingsRequest(req, res)) return
17
15
  if (req.method !== 'POST') {
18
16
  return writeJson(res, 405, { ok: false, error: 'POST only' })
19
17
  }
@@ -69,13 +67,11 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
69
67
  path: '/dsh-clinebot/models/toggle',
70
68
  handler: async (req, res) => {
71
69
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
72
- if (!isTrustedSettingsRequest(req)) {
73
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
74
- }
70
+ if (!assertTrustedSettingsRequest(req, res)) return
75
71
  try {
76
72
  const bodyBuf = await readBody(req)
77
73
  let body = {}
78
- try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
74
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
79
75
 
80
76
  const settingsApi = getSettingsApi()
81
77
  if (settingsApi?.replace) {
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, Config } from '../config.js'
4
4
  import { buildStatus } from '../provider-sync.js'
5
5
  import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
@@ -62,13 +62,11 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
62
62
  path: '/dsh-clinebot/save-key',
63
63
  handler: async (req, res) => {
64
64
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
65
- if (!isTrustedSettingsRequest(req)) {
66
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
67
- }
65
+ if (!assertTrustedSettingsRequest(req, res)) return
68
66
  try {
69
67
  const bodyBuf = await readBody(req)
70
68
  let body = {}
71
- try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
69
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
72
70
 
73
71
  const apiKey = String(body.apiKey || '').trim()
74
72
  if (!apiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
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",