@goodandready/dsh-kanban 0.2.29 → 0.2.30
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 +46 -20
- package/lib/routes.js +107 -34
- package/lib/updater.js +14 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import { registerPluginUpdater } from './updater.js'
|
|
|
12
12
|
import {
|
|
13
13
|
MAX_BODY_BYTES,
|
|
14
14
|
isTrustedRequest,
|
|
15
|
+
rejectUntrustedRequest,
|
|
16
|
+
getRejectionStatus,
|
|
15
17
|
parseTaskPath,
|
|
16
18
|
buildBoard,
|
|
17
19
|
applyMove,
|
|
@@ -634,12 +636,36 @@ export function apply(ctx, config) {
|
|
|
634
636
|
return () => { rearmers.delete(arm); clearInterval(timer) }
|
|
635
637
|
}, 'dsh-kanban: sync timer')
|
|
636
638
|
|
|
639
|
+
const resolveConnection = () => {
|
|
640
|
+
try {
|
|
641
|
+
if (ctx && typeof ctx.get === 'function') {
|
|
642
|
+
const c = ctx.get('connection')
|
|
643
|
+
if (c) return c
|
|
644
|
+
}
|
|
645
|
+
} catch { /* ignore */ }
|
|
646
|
+
try {
|
|
647
|
+
if (ctx && ctx.connection) {
|
|
648
|
+
return ctx.connection
|
|
649
|
+
}
|
|
650
|
+
} catch { /* ignore */ }
|
|
651
|
+
return undefined
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const routeOptions = () => ({
|
|
655
|
+
get connection() {
|
|
656
|
+
return resolveConnection()
|
|
657
|
+
},
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
const isTrusted = (req) => isTrustedRequest(req, routeOptions())
|
|
661
|
+
const rejectUntrusted = (req, res) => rejectUntrustedRequest(req, res, routeOptions())
|
|
662
|
+
|
|
637
663
|
ctx.effect(() => ctx.webServer.register({
|
|
638
664
|
kind: 'exact',
|
|
639
665
|
path: '/dsh-kanban/project-task',
|
|
640
666
|
handler: async (req, res) => {
|
|
641
667
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
642
|
-
if (
|
|
668
|
+
if (rejectUntrusted(req, res)) return
|
|
643
669
|
try {
|
|
644
670
|
const input = await readBody(req)
|
|
645
671
|
reply(res, await createProjectTask({
|
|
@@ -661,7 +687,7 @@ export function apply(ctx, config) {
|
|
|
661
687
|
// Both services are optional in minimal environments. Empty lists render gracefully.
|
|
662
688
|
handler: async (req, res) => {
|
|
663
689
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
664
|
-
if (
|
|
690
|
+
if (rejectUntrusted(req, res)) return
|
|
665
691
|
const presets = ctx.get('agentPresets')
|
|
666
692
|
const permissions = ctx.get('permissionPresets')
|
|
667
693
|
let agentPresets = []
|
|
@@ -698,7 +724,7 @@ export function apply(ctx, config) {
|
|
|
698
724
|
path: '/dsh-kanban/sessions',
|
|
699
725
|
handler: (req, res) => {
|
|
700
726
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
701
|
-
if (
|
|
727
|
+
if (rejectUntrusted(req, res)) return
|
|
702
728
|
reply(res, { sessions: liveSessions({ store, agents: ctx.agents }) })
|
|
703
729
|
},
|
|
704
730
|
}), 'dsh-kanban: live sessions')
|
|
@@ -708,7 +734,7 @@ export function apply(ctx, config) {
|
|
|
708
734
|
path: '/dsh-kanban/batch',
|
|
709
735
|
handler: async (req, res) => {
|
|
710
736
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
711
|
-
if (
|
|
737
|
+
if (rejectUntrusted(req, res)) return
|
|
712
738
|
try {
|
|
713
739
|
const input = await readBody(req)
|
|
714
740
|
const ids = Array.isArray(input.ids) ? input.ids : []
|
|
@@ -740,7 +766,7 @@ export function apply(ctx, config) {
|
|
|
740
766
|
path: '/dsh-kanban/archive',
|
|
741
767
|
handler: (req, res) => {
|
|
742
768
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
743
|
-
if (
|
|
769
|
+
if (rejectUntrusted(req, res)) return
|
|
744
770
|
reply(res, listArchive({ store }))
|
|
745
771
|
},
|
|
746
772
|
}), 'dsh-kanban: archive')
|
|
@@ -750,7 +776,7 @@ export function apply(ctx, config) {
|
|
|
750
776
|
path: '/dsh-kanban/sync',
|
|
751
777
|
handler: async (req, res) => {
|
|
752
778
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
753
|
-
if (
|
|
779
|
+
if (rejectUntrusted(req, res)) return
|
|
754
780
|
try {
|
|
755
781
|
writeJson(res, 200, await runSync())
|
|
756
782
|
} catch (error) {
|
|
@@ -846,7 +872,7 @@ export function apply(ctx, config) {
|
|
|
846
872
|
path: '/dsh-kanban/board',
|
|
847
873
|
handler: (req, res) => {
|
|
848
874
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
849
|
-
if (
|
|
875
|
+
if (rejectUntrusted(req, res)) return
|
|
850
876
|
const url = urlOf(req)
|
|
851
877
|
reply(res, buildBoard({
|
|
852
878
|
store,
|
|
@@ -881,7 +907,7 @@ export function apply(ctx, config) {
|
|
|
881
907
|
path: '/dsh-kanban/metrics',
|
|
882
908
|
handler: (req, res) => {
|
|
883
909
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
884
|
-
if (
|
|
910
|
+
if (rejectUntrusted(req, res)) return
|
|
885
911
|
const url = urlOf(req)
|
|
886
912
|
const days = Number(url.searchParams.get('staleDays'))
|
|
887
913
|
reply(res, boardMetrics({
|
|
@@ -898,7 +924,7 @@ export function apply(ctx, config) {
|
|
|
898
924
|
// Empty login indicates unconfigured or unreachable Gitea; client omits "Mine" filter.
|
|
899
925
|
handler: async (req, res) => {
|
|
900
926
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
901
|
-
if (
|
|
927
|
+
if (rejectUntrusted(req, res)) return
|
|
902
928
|
reply(res, { login: await whoAmI() })
|
|
903
929
|
},
|
|
904
930
|
}), 'dsh-kanban: whoami')
|
|
@@ -908,7 +934,7 @@ export function apply(ctx, config) {
|
|
|
908
934
|
path: '/dsh-kanban/export.csv',
|
|
909
935
|
handler: async (req, res) => {
|
|
910
936
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
911
|
-
if (
|
|
937
|
+
if (rejectUntrusted(req, res)) return
|
|
912
938
|
const u = urlOf(req)
|
|
913
939
|
const board = u.searchParams.get('board') || 'main'
|
|
914
940
|
const csv = exportTasksCsv({ store, board })
|
|
@@ -925,7 +951,7 @@ export function apply(ctx, config) {
|
|
|
925
951
|
kind: 'exact',
|
|
926
952
|
path: '/dsh-kanban/events',
|
|
927
953
|
handler: (req, res) => {
|
|
928
|
-
eventHub.handleSseRequest(req, res, { isTrustedRequest })
|
|
954
|
+
eventHub.handleSseRequest(req, res, { isTrustedRequest: isTrusted })
|
|
929
955
|
},
|
|
930
956
|
}), 'dsh-kanban: sse events stream')
|
|
931
957
|
|
|
@@ -941,7 +967,7 @@ export function apply(ctx, config) {
|
|
|
941
967
|
path: '/dsh-kanban/templates',
|
|
942
968
|
handler: (req, res) => {
|
|
943
969
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
944
|
-
if (
|
|
970
|
+
if (rejectUntrusted(req, res)) return
|
|
945
971
|
const u = new URL(req.url, 'http://localhost')
|
|
946
972
|
const tid = u.searchParams.get('id')
|
|
947
973
|
if (tid) {
|
|
@@ -958,7 +984,7 @@ export function apply(ctx, config) {
|
|
|
958
984
|
kind: 'exact',
|
|
959
985
|
path: '/dsh-kanban/snapshot',
|
|
960
986
|
handler: async (req, res) => {
|
|
961
|
-
if (
|
|
987
|
+
if (rejectUntrusted(req, res)) return
|
|
962
988
|
if (req.method === 'GET') { reply(res, exportBoard({ store })); return }
|
|
963
989
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
964
990
|
try {
|
|
@@ -984,7 +1010,7 @@ export function apply(ctx, config) {
|
|
|
984
1010
|
// Path without ID handles local task creation.
|
|
985
1011
|
if (pathname === '/dsh-kanban/task' || pathname === '/dsh-kanban/task/') {
|
|
986
1012
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
987
|
-
if (
|
|
1013
|
+
if (rejectUntrusted(req, res)) return
|
|
988
1014
|
try {
|
|
989
1015
|
const resObj = createTask({ store, input: await readBody(req) })
|
|
990
1016
|
if (resObj?.task) {
|
|
@@ -1001,7 +1027,7 @@ export function apply(ctx, config) {
|
|
|
1001
1027
|
if (parsed === undefined) { writeJson(res, 404, { error: 'not-found' }); return }
|
|
1002
1028
|
const { id, action } = parsed
|
|
1003
1029
|
|
|
1004
|
-
if (
|
|
1030
|
+
if (rejectUntrusted(req, res)) return
|
|
1005
1031
|
|
|
1006
1032
|
if (req.method === 'GET' && action === 'log') { reply(res, taskLog({ store, id })); return }
|
|
1007
1033
|
if (req.method === 'GET' && action === 'body') { reply(res, taskBody({ store, id })); return }
|
|
@@ -1393,7 +1419,7 @@ export function apply(ctx, config) {
|
|
|
1393
1419
|
path: '/dsh-kanban/gitea/repos',
|
|
1394
1420
|
handler: async (req, res) => {
|
|
1395
1421
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1396
|
-
if (
|
|
1422
|
+
if (rejectUntrusted(req, res)) return
|
|
1397
1423
|
const url = urlOf(req)
|
|
1398
1424
|
reply(res, await searchRepos({ gitea: git, query: url.searchParams.get('q') || '' }))
|
|
1399
1425
|
},
|
|
@@ -1404,7 +1430,7 @@ export function apply(ctx, config) {
|
|
|
1404
1430
|
path: '/dsh-kanban/gitea/issues',
|
|
1405
1431
|
handler: async (req, res) => {
|
|
1406
1432
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1407
|
-
if (
|
|
1433
|
+
if (rejectUntrusted(req, res)) return
|
|
1408
1434
|
const url = urlOf(req)
|
|
1409
1435
|
reply(res, await listImportable({
|
|
1410
1436
|
gitea: git,
|
|
@@ -1421,7 +1447,7 @@ export function apply(ctx, config) {
|
|
|
1421
1447
|
path: '/dsh-kanban/import',
|
|
1422
1448
|
handler: async (req, res) => {
|
|
1423
1449
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1424
|
-
if (
|
|
1450
|
+
if (rejectUntrusted(req, res)) return
|
|
1425
1451
|
try {
|
|
1426
1452
|
const input = await readBody(req)
|
|
1427
1453
|
reply(res, await importIssue({
|
|
@@ -1442,7 +1468,7 @@ export function apply(ctx, config) {
|
|
|
1442
1468
|
path: '/dsh-kanban/models',
|
|
1443
1469
|
handler: async (req, res) => {
|
|
1444
1470
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1445
|
-
if (
|
|
1471
|
+
if (rejectUntrusted(req, res)) return
|
|
1446
1472
|
const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null
|
|
1447
1473
|
const llmService = ctx.get('llm')
|
|
1448
1474
|
let providers = []
|
|
@@ -1471,7 +1497,7 @@ export function apply(ctx, config) {
|
|
|
1471
1497
|
path: '/dsh-kanban/session',
|
|
1472
1498
|
handler: (req, res) => {
|
|
1473
1499
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1474
|
-
if (
|
|
1500
|
+
if (rejectUntrusted(req, res)) return
|
|
1475
1501
|
const m = /^\/dsh-kanban\/session\/([^/]+)\/task\/?$/.exec(urlOf(req).pathname)
|
|
1476
1502
|
if (m === null) { writeJson(res, 404, { error: 'not-found' }); return }
|
|
1477
1503
|
reply(res, taskBySession({ store, sessionId: decodeURIComponent(m[1]), agents: ctx.agents }))
|
package/lib/routes.js
CHANGED
|
@@ -57,40 +57,75 @@ export function isTrustedHost(host) {
|
|
|
57
57
|
return false
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export function getClientIp(req) {
|
|
61
|
+
return (
|
|
62
|
+
req?.socket?.remoteAddress ||
|
|
63
|
+
req?.connection?.remoteAddress ||
|
|
64
|
+
req?.info?.remoteAddress ||
|
|
65
|
+
''
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
/**
|
|
61
|
-
* Cross-site request
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
70
|
+
* Cross-site request, DNS rebinding, and authentication validation (#261, #369, #384).
|
|
71
|
+
*
|
|
72
|
+
* Requirements:
|
|
73
|
+
* 1. If DSH connection service is available (options.connection), requestRejection(req)
|
|
74
|
+
* is the authoritative gate for browser session authentication and trusted network access.
|
|
75
|
+
* If rejection is returned, the numeric status (e.g. 401 or 403) is returned.
|
|
76
|
+
* 2. Host header is required and must target a trusted host (loopback, LAN, .local).
|
|
77
|
+
* 3. Cross-site requests ('sec-fetch-site: cross-site' or origin mismatch) are rejected with 403.
|
|
78
|
+
* 4. Origin & Referer headers (if present) must match Host or a trusted host.
|
|
79
|
+
* 5. Network boundary validation:
|
|
80
|
+
* - Loopback callers (127.0.0.1, ::1) are allowed.
|
|
81
|
+
* - Non-loopback / LAN callers require valid connection authentication (fail-closed, return 401).
|
|
82
|
+
* - Unknown remote address (socket present but empty remoteAddress) fails closed with 403.
|
|
83
|
+
*
|
|
84
|
+
* @param {import('node:http').IncomingMessage} req
|
|
85
|
+
* @param {{ connection?: { requestRejection?: (req: any) => number | undefined } }} [options]
|
|
86
|
+
* @returns {number|undefined} Rejection HTTP status code (401, 403), or undefined if allowed.
|
|
67
87
|
*/
|
|
68
|
-
export function
|
|
69
|
-
|
|
88
|
+
export function getRejectionStatus(req, options = {}) {
|
|
89
|
+
const connection = options?.connection
|
|
90
|
+
if (connection && typeof connection.requestRejection === 'function') {
|
|
91
|
+
try {
|
|
92
|
+
const rejection = connection.requestRejection(req)
|
|
93
|
+
if (rejection !== undefined) {
|
|
94
|
+
if (typeof rejection === 'number' && Number.isInteger(rejection) && rejection >= 400 && rejection < 600) {
|
|
95
|
+
return rejection
|
|
96
|
+
}
|
|
97
|
+
return rejection === 401 ? 401 : 403
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Fail closed if connection service throws
|
|
101
|
+
return 403
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!req || !req.headers) return 403
|
|
70
106
|
|
|
71
107
|
const host = req.headers.host
|
|
72
108
|
if (!host || !isTrustedHost(host)) {
|
|
73
|
-
return
|
|
109
|
+
return 403
|
|
74
110
|
}
|
|
75
111
|
|
|
76
|
-
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress
|
|
77
112
|
const site = req.headers['sec-fetch-site']
|
|
78
113
|
|
|
79
114
|
// 1. Explicit cross-site or same-site requests are rejected
|
|
80
115
|
if (site === 'cross-site' || site === 'same-site') {
|
|
81
|
-
return
|
|
116
|
+
return 403
|
|
82
117
|
}
|
|
83
118
|
|
|
84
119
|
// 2. Validate origin against trusted hosts and host header
|
|
85
120
|
const origin = req.headers.origin
|
|
86
121
|
if (origin !== undefined) {
|
|
87
|
-
if (origin === 'null' || !origin) return
|
|
122
|
+
if (origin === 'null' || !origin) return 403
|
|
88
123
|
try {
|
|
89
124
|
const parsed = new URL(origin)
|
|
90
|
-
if (!isTrustedHost(parsed.hostname)) return
|
|
91
|
-
if (parsed.host && parsed.host !== host) return
|
|
125
|
+
if (!isTrustedHost(parsed.hostname)) return 403
|
|
126
|
+
if (parsed.host && parsed.host !== host) return 403
|
|
92
127
|
} catch {
|
|
93
|
-
return
|
|
128
|
+
return 403
|
|
94
129
|
}
|
|
95
130
|
}
|
|
96
131
|
|
|
@@ -99,36 +134,74 @@ export function isTrustedRequest(req) {
|
|
|
99
134
|
if (referer !== undefined && referer) {
|
|
100
135
|
try {
|
|
101
136
|
const parsedRef = new URL(referer)
|
|
102
|
-
if (!isTrustedHost(parsedRef.hostname)) return
|
|
103
|
-
if (parsedRef.host && parsedRef.host !== host) return
|
|
137
|
+
if (!isTrustedHost(parsedRef.hostname)) return 403
|
|
138
|
+
if (parsedRef.host && parsedRef.host !== host) return 403
|
|
104
139
|
} catch {
|
|
105
|
-
return
|
|
140
|
+
return 403
|
|
106
141
|
}
|
|
107
142
|
}
|
|
108
143
|
|
|
109
|
-
// 4.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
144
|
+
// 4. Validate remote address and network boundary
|
|
145
|
+
const hasSocket = !!(req.socket || req.connection)
|
|
146
|
+
const remote = getClientIp(req)
|
|
147
|
+
|
|
148
|
+
if (hasSocket) {
|
|
149
|
+
if (!remote) {
|
|
150
|
+
return 403
|
|
151
|
+
}
|
|
152
|
+
if (!isLoopbackAddress(remote) && !isLanAddress(remote)) {
|
|
153
|
+
return 403
|
|
154
|
+
}
|
|
155
|
+
// Remote / LAN callers without connection authentication are rejected (#384)
|
|
156
|
+
if (!isLoopbackAddress(remote)) {
|
|
157
|
+
if (!connection) {
|
|
158
|
+
return 401
|
|
159
|
+
}
|
|
160
|
+
}
|
|
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
|
|
114
166
|
}
|
|
115
|
-
return true
|
|
116
167
|
}
|
|
117
168
|
|
|
118
|
-
// 5.
|
|
119
|
-
//
|
|
120
|
-
if (
|
|
121
|
-
return
|
|
169
|
+
// 5. Browser request with declared site:
|
|
170
|
+
// Must be same-origin or none
|
|
171
|
+
if (site && site !== 'same-origin' && site !== 'none') {
|
|
172
|
+
return 403
|
|
122
173
|
}
|
|
123
174
|
|
|
124
|
-
// 6.
|
|
125
|
-
if (!remote &&
|
|
126
|
-
|
|
127
|
-
if (isLoopbackAddress(hostName)) {
|
|
128
|
-
return true
|
|
129
|
-
}
|
|
175
|
+
// 6. Request without declared site from non-loopback requires connection
|
|
176
|
+
if (!site && remote && !isLoopbackAddress(remote) && !connection) {
|
|
177
|
+
return 401
|
|
130
178
|
}
|
|
131
179
|
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isTrustedRequest(req, options = {}) {
|
|
184
|
+
return getRejectionStatus(req, options) === undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function rejectUntrustedRequest(req, res, options = {}) {
|
|
188
|
+
const status = getRejectionStatus(req, options)
|
|
189
|
+
if (status !== undefined) {
|
|
190
|
+
try {
|
|
191
|
+
res.writeHead(status, {
|
|
192
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
193
|
+
'Cache-Control': 'no-store',
|
|
194
|
+
})
|
|
195
|
+
res.end(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
error: status === 401 ? 'unauthorized' : 'cross-site',
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
} catch {
|
|
201
|
+
/* socket closed */
|
|
202
|
+
}
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
132
205
|
return false
|
|
133
206
|
}
|
|
134
207
|
|
package/lib/updater.js
CHANGED
|
@@ -42,8 +42,20 @@ function isPrivateLan(value) {
|
|
|
42
42
|
)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function isTrustedUpdateRequest(request) {
|
|
45
|
+
export function isTrustedUpdateRequest(request, targetCtx = null) {
|
|
46
46
|
if (header(request, UPDATE_HEADER) !== '1') return false
|
|
47
|
+
const conn = (targetCtx && typeof targetCtx.get === 'function' ? targetCtx.get('connection') : null)
|
|
48
|
+
|| targetCtx?.connection
|
|
49
|
+
|| request?.connectionService
|
|
50
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
51
|
+
let rejection
|
|
52
|
+
try {
|
|
53
|
+
rejection = conn.requestRejection(request)
|
|
54
|
+
} catch {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
if (rejection !== undefined) return false
|
|
58
|
+
}
|
|
47
59
|
const remote = request.socket?.remoteAddress
|
|
48
60
|
if (!isLoopback(remote) && !isPrivateLan(remote)) return false
|
|
49
61
|
const site = header(request, 'sec-fetch-site')
|
|
@@ -300,7 +312,7 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
300
312
|
response.end()
|
|
301
313
|
return
|
|
302
314
|
}
|
|
303
|
-
if (!isTrustedUpdateRequest(request)) {
|
|
315
|
+
if (!isTrustedUpdateRequest(request, ctx)) {
|
|
304
316
|
json(response, 403, { error: 'Rejected non-local or cross-origin update request.' })
|
|
305
317
|
return
|
|
306
318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.30",
|
|
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",
|