@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 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
- return (
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 if (!connection) {
162
- // Synthetic mock request without socket: allow loopback host for unit tests
163
- const hostname = String(host).split(':')[0].toLowerCase()
164
- if (!isLoopbackAddress(hostname) && hostname !== 'localhost') {
165
- return 401
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 remote = request.socket?.remoteAddress
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.30",
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",