@goodandready/dsh-kanban 0.2.30 → 0.2.31
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/lib/index.js +1 -1
- package/lib/routes.js +90 -7
- package/lib/updater.js +8 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -111,7 +111,7 @@ export const name = '@goodandready/dsh-kanban'
|
|
|
111
111
|
* Services are injected where needed. Declaring an unavailable service prevents
|
|
112
112
|
* the plugin from loading entirely.
|
|
113
113
|
*/
|
|
114
|
-
export const inject = ['settings', 'webServer', 'agents', 'agentDefaultModel', 'credentials', 'llm', 'tools']
|
|
114
|
+
export const inject = ['settings', 'webServer', 'agents', 'agentDefaultModel', 'credentials', 'llm', 'tools', 'connection']
|
|
115
115
|
|
|
116
116
|
/** Settings namespace string matching client bundle definition. */
|
|
117
117
|
// Constant string defined in this file; identical across core versions.
|
package/lib/routes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
1
2
|
// HTTP route handlers for the board.
|
|
2
3
|
//
|
|
3
4
|
// The client bundle runs in the browser without direct access to SQLite or host services;
|
|
@@ -57,13 +58,78 @@ export function isTrustedHost(host) {
|
|
|
57
58
|
return false
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
export function normalizeIp(addr) {
|
|
62
|
+
if (!addr || typeof addr !== 'string') return ''
|
|
63
|
+
return addr.toLowerCase().replace(/^::ffff:/, '').replace(/^\[|\]$/g, '')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function cookieNameForAuthority(authority) {
|
|
67
|
+
if (!authority || typeof authority !== 'string') return ''
|
|
68
|
+
const hash = crypto.createHash('sha256').update(authority.trim()).digest('base64url')
|
|
69
|
+
return 'dsh-auth-' + hash
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isLoopbackCookieName(name, host = '') {
|
|
73
|
+
if (!name || typeof name !== 'string' || !name.startsWith('dsh-auth-')) return false
|
|
74
|
+
const authorities = [
|
|
75
|
+
host,
|
|
76
|
+
'127.0.0.1:3000',
|
|
77
|
+
'127.0.0.1:3080',
|
|
78
|
+
'127.0.0.1:3082',
|
|
79
|
+
'127.0.0.1',
|
|
80
|
+
'localhost:3000',
|
|
81
|
+
'localhost:3080',
|
|
82
|
+
'localhost:3082',
|
|
83
|
+
'localhost',
|
|
84
|
+
'[::1]:3000',
|
|
85
|
+
'[::1]',
|
|
86
|
+
]
|
|
87
|
+
for (const auth of authorities) {
|
|
88
|
+
if (auth && isLoopbackAddress(auth.split(':')[0])) {
|
|
89
|
+
if (cookieNameForAuthority(auth) === name) return true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hasClientCredentials(req) {
|
|
96
|
+
if (!req || !req.headers) return false
|
|
97
|
+
if (req.headers.authorization) return true
|
|
98
|
+
if (req.headers['x-dsh-lan-pin']) return true
|
|
99
|
+
const cookie = req.headers.cookie
|
|
100
|
+
if (!cookie || typeof cookie !== 'string') return false
|
|
101
|
+
if (cookie.includes('dsh_token=') || cookie.includes('dsh_lan_pin=')) return true
|
|
102
|
+
|
|
103
|
+
const parts = cookie.split(';')
|
|
104
|
+
for (const part of parts) {
|
|
105
|
+
const trimmed = part.trim()
|
|
106
|
+
const eqIdx = trimmed.indexOf('=')
|
|
107
|
+
if (eqIdx === -1) continue
|
|
108
|
+
const name = trimmed.slice(0, eqIdx).trim()
|
|
109
|
+
if (name.startsWith('dsh-auth-')) {
|
|
110
|
+
if (!isLoopbackCookieName(name, req.headers.host)) {
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
export function getClientIp(req) {
|
|
61
|
-
|
|
119
|
+
const socketAddress =
|
|
62
120
|
req?.socket?.remoteAddress ||
|
|
63
121
|
req?.connection?.remoteAddress ||
|
|
64
122
|
req?.info?.remoteAddress ||
|
|
65
123
|
''
|
|
66
|
-
)
|
|
124
|
+
const socketIp = normalizeIp(socketAddress)
|
|
125
|
+
const xff = req?.headers?.['x-forwarded-for']
|
|
126
|
+
if (xff && typeof xff === 'string' && (isLoopbackAddress(socketIp) || !socketIp)) {
|
|
127
|
+
const first = xff.split(',')[0].trim()
|
|
128
|
+
if (first) {
|
|
129
|
+
return normalizeIp(first)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return socketIp
|
|
67
133
|
}
|
|
68
134
|
|
|
69
135
|
/**
|
|
@@ -157,12 +223,29 @@ export function getRejectionStatus(req, options = {}) {
|
|
|
157
223
|
if (!connection) {
|
|
158
224
|
return 401
|
|
159
225
|
}
|
|
226
|
+
// If request was forwarded through a reverse proxy (x-forwarded-for),
|
|
227
|
+
// the proxy may have injected loopback credentials. Validate that the client
|
|
228
|
+
// itself supplied authentic credentials (bearer token, PIN, or client session cookie).
|
|
229
|
+
const xff = req.headers['x-forwarded-for']
|
|
230
|
+
if (xff && !hasClientCredentials(req)) {
|
|
231
|
+
return 401
|
|
232
|
+
}
|
|
160
233
|
}
|
|
161
|
-
} else
|
|
162
|
-
// Synthetic mock request without socket
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
|
|
234
|
+
} else {
|
|
235
|
+
// Synthetic mock request without socket
|
|
236
|
+
const xff = req.headers['x-forwarded-for']
|
|
237
|
+
if (xff) {
|
|
238
|
+
const first = xff.split(',')[0].trim()
|
|
239
|
+
if (first && !isLoopbackAddress(normalizeIp(first))) {
|
|
240
|
+
if (!connection || !hasClientCredentials(req)) {
|
|
241
|
+
return 401
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else if (!connection) {
|
|
245
|
+
const hostname = String(host).split(':')[0].toLowerCase()
|
|
246
|
+
if (!isLoopbackAddress(hostname) && hostname !== 'localhost') {
|
|
247
|
+
return 401
|
|
248
|
+
}
|
|
166
249
|
}
|
|
167
250
|
}
|
|
168
251
|
|
package/lib/updater.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process'
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
|
+
import { hasClientCredentials } from './routes.js'
|
|
5
6
|
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
8
|
|
|
@@ -56,8 +57,14 @@ export function isTrustedUpdateRequest(request, targetCtx = null) {
|
|
|
56
57
|
}
|
|
57
58
|
if (rejection !== undefined) return false
|
|
58
59
|
}
|
|
59
|
-
const
|
|
60
|
+
const socketAddress = request.socket?.remoteAddress
|
|
61
|
+
const xff = header(request, 'x-forwarded-for')
|
|
62
|
+
let remote = socketAddress
|
|
63
|
+
if (xff && isLoopback(socketAddress)) {
|
|
64
|
+
remote = xff.split(',')[0].trim()
|
|
65
|
+
}
|
|
60
66
|
if (!isLoopback(remote) && !isPrivateLan(remote)) return false
|
|
67
|
+
if (!isLoopback(remote) && xff && !hasClientCredentials(request)) return false
|
|
61
68
|
const site = header(request, 'sec-fetch-site')
|
|
62
69
|
if (!site || (site !== 'same-origin' && site !== 'same-site' && site !== 'none')) return false
|
|
63
70
|
const origin = header(request, 'origin')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Visual Kanban board for DeepSeek Harness: Gitea and GitHub-backed tasks, workflow columns, and a dedicated agent session per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|