@goodandready/dsh-clinebot 0.5.22 → 0.5.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 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.24] - 2026-10-13
9
+
10
+ ### Security
11
+ - **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.
12
+
13
+ ## [0.5.23] - 2026-10-13
14
+
15
+ ### Removed
16
+ - **Dead Exports & Helper Hygiene (Issue #220)**: Removed unused `getOrStartProxyPort` export from `lib/proxy-server.js` and unused cursor helper exports `getRoundRobinCursor` and `resetRoundRobinCursor` from `lib/session-router.js`. Internal round-robin cursor rotation and lifecycle resets remain fully operational.
17
+
8
18
  ## [0.5.22] - 2026-10-13
9
19
 
10
20
  ### Added
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/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
@@ -136,10 +136,3 @@ export async function stopLoopbackProxyServer() {
136
136
  })
137
137
  }
138
138
 
139
- export async function getOrStartProxyPort(ctx, routeEnv) {
140
- if (activeProxyPort) return activeProxyPort
141
- if (routeEnv) {
142
- return await startLoopbackProxyServer(ctx, routeEnv)
143
- }
144
- return null
145
- }
@@ -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.22'
17
+ let packageVersion = '0.5.24'
18
18
  try {
19
- packageVersion = require('../package.json').version || '0.5.22'
19
+ packageVersion = require('../package.json').version || '0.5.24'
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
@@ -132,14 +132,6 @@ export function getAccountUsageMetric(acc) {
132
132
 
133
133
  let roundRobinCursor = 0
134
134
 
135
- export function getRoundRobinCursor() {
136
- return roundRobinCursor
137
- }
138
-
139
- export function resetRoundRobinCursor() {
140
- roundRobinCursor = 0
141
- }
142
-
143
135
  export function getNextRoundRobinAccount(accounts = []) {
144
136
  if (!Array.isArray(accounts) || accounts.length === 0) {
145
137
  return null
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.22",
3
+ "version": "0.5.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",