@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 +10 -0
- package/lib/access.js +21 -4
- package/lib/http.js +28 -2
- package/lib/proxy-server.js +0 -7
- 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/session-router.js +0 -8
- 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.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
|
-
|
|
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
|
-
|
|
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/proxy-server.js
CHANGED
|
@@ -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
|
-
}
|
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.24'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
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
|
}
|
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/session-router.js
CHANGED
|
@@ -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.
|
|
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",
|