@goodandready/dsh-agent-orchestrator 0.1.6
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/LICENSE +22 -0
- package/README.md +184 -0
- package/README.ru.md +86 -0
- package/README.zh.md +64 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1374 -0
- package/lib/http-guard.js +166 -0
- package/lib/index.js +950 -0
- package/lib/integrations/kanban-bridge.js +102 -0
- package/lib/pipeline/cache-prefixer.js +226 -0
- package/lib/pipeline/concurrency-gate.js +110 -0
- package/lib/pipeline/dag-engine.js +290 -0
- package/lib/pipeline/decision-trace.js +124 -0
- package/lib/pipeline/decomposer.js +102 -0
- package/lib/pipeline/delegation.js +625 -0
- package/lib/pipeline/guidance.js +94 -0
- package/lib/pipeline/intent.js +98 -0
- package/lib/pipeline/intersection.js +235 -0
- package/lib/pipeline/model-selection.js +742 -0
- package/lib/pipeline/preset-sync.js +84 -0
- package/lib/pipeline/scenarios.js +468 -0
- package/lib/pipeline/session-lifecycle.js +520 -0
- package/lib/pipeline/snapshots.js +240 -0
- package/lib/pipeline/token-watchdog.js +101 -0
- package/lib/pipeline/worker-pool.js +143 -0
- package/lib/routes.js +277 -0
- package/lib/store.js +176 -0
- package/package.json +94 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP guard utilities for protecting write endpoints (POST/PUT/DELETE)
|
|
3
|
+
* against drive-by CSRF, unauthorized cross-network access, and cross-origin tampering.
|
|
4
|
+
*
|
|
5
|
+
* Implements Issue #113:
|
|
6
|
+
* 1. Loopback / Origin / Host validation
|
|
7
|
+
* 2. Sec-Fetch-Site and Referer verification
|
|
8
|
+
* 3. Body size limiter (default 1MB) with fail-closed rejection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_MAX_BODY_BYTES = 1024 * 1024 // 1MB
|
|
12
|
+
|
|
13
|
+
export function isLoopbackAddress(ip) {
|
|
14
|
+
if (!ip || typeof ip !== 'string') return false
|
|
15
|
+
return (
|
|
16
|
+
ip === '127.0.0.1' ||
|
|
17
|
+
ip === '::1' ||
|
|
18
|
+
ip === '::ffff:127.0.0.1' ||
|
|
19
|
+
ip.startsWith('127.')
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getClientIp(req) {
|
|
24
|
+
return (
|
|
25
|
+
req.socket?.remoteAddress ||
|
|
26
|
+
req.connection?.remoteAddress ||
|
|
27
|
+
req.info?.remoteAddress ||
|
|
28
|
+
''
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Checks whether an incoming HTTP request is safe for mutation or sensitive dispatch.
|
|
34
|
+
*
|
|
35
|
+
* Requirements:
|
|
36
|
+
* 1. If Origin header is present:
|
|
37
|
+
* - must not be "null" or empty
|
|
38
|
+
* - parsed Origin host must match Host header
|
|
39
|
+
* 2. If Referer header is present:
|
|
40
|
+
* - parsed Referer host must match Host header
|
|
41
|
+
* 3. If Sec-Fetch-Site header is present:
|
|
42
|
+
* - must be "same-origin" or "none" (cross-site and same-site are rejected)
|
|
43
|
+
* 4. If neither Origin nor Sec-Fetch-Site is present:
|
|
44
|
+
* - only local loopback clients (127.0.0.1, ::1) are permitted.
|
|
45
|
+
*/
|
|
46
|
+
export function isTrustedWriteRequest(req) {
|
|
47
|
+
const headers = req.headers || {}
|
|
48
|
+
const host = headers.host || ''
|
|
49
|
+
|
|
50
|
+
const origin = headers.origin
|
|
51
|
+
if (origin !== undefined) {
|
|
52
|
+
if (origin === 'null' || !origin) return false
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(origin)
|
|
55
|
+
if (parsed.host && host && parsed.host !== host) return false
|
|
56
|
+
} catch {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const referer = headers.referer
|
|
62
|
+
if (referer !== undefined && referer) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(referer)
|
|
65
|
+
if (parsed.host && host && parsed.host !== host) return false
|
|
66
|
+
} catch {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const site = headers['sec-fetch-site']
|
|
72
|
+
if (site !== undefined && site) {
|
|
73
|
+
if (site !== 'same-origin' && site !== 'none') return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If both origin and sec-fetch-site are absent, permit only loopback callers
|
|
77
|
+
if (origin === undefined && site === undefined) {
|
|
78
|
+
const ip = getClientIp(req)
|
|
79
|
+
if (!isLoopbackAddress(ip)) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function rejectUntrustedRequest(req, res) {
|
|
88
|
+
if (!isTrustedWriteRequest(req)) {
|
|
89
|
+
try {
|
|
90
|
+
res.statusCode = 403
|
|
91
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
92
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
93
|
+
res.end(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
ok: false,
|
|
96
|
+
error: {
|
|
97
|
+
code: 'forbidden',
|
|
98
|
+
message: 'Forbidden: same-origin or local loopback only',
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
} catch {
|
|
103
|
+
/* socket closed */
|
|
104
|
+
}
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parses JSON request body with explicit payload size limit (Issue #113).
|
|
112
|
+
* Throws an error if body exceeds maxBytes or is invalid JSON.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('http').IncomingMessage} req
|
|
115
|
+
* @param {number} [maxBytes=DEFAULT_MAX_BODY_BYTES]
|
|
116
|
+
* @returns {Promise<object>}
|
|
117
|
+
*/
|
|
118
|
+
export function parseBoundedJsonBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
let body = ''
|
|
121
|
+
let receivedBytes = 0
|
|
122
|
+
|
|
123
|
+
const onData = (chunk) => {
|
|
124
|
+
receivedBytes += chunk.length
|
|
125
|
+
if (receivedBytes > maxBytes) {
|
|
126
|
+
req.pause()
|
|
127
|
+
cleanup()
|
|
128
|
+
const err = new Error(`Payload Too Large: body exceeds limit of ${maxBytes} bytes`)
|
|
129
|
+
err.statusCode = 413
|
|
130
|
+
reject(err)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
body += chunk
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const onEnd = () => {
|
|
137
|
+
cleanup()
|
|
138
|
+
if (!body || body.trim() === '') {
|
|
139
|
+
return resolve({})
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(body)
|
|
143
|
+
resolve(parsed)
|
|
144
|
+
} catch (e) {
|
|
145
|
+
const err = new Error(`Invalid JSON body: ${e.message}`)
|
|
146
|
+
err.statusCode = 400
|
|
147
|
+
reject(err)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const onError = (err) => {
|
|
152
|
+
cleanup()
|
|
153
|
+
reject(err)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function cleanup() {
|
|
157
|
+
req.removeListener('data', onData)
|
|
158
|
+
req.removeListener('end', onEnd)
|
|
159
|
+
req.removeListener('error', onError)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
req.on('data', onData)
|
|
163
|
+
req.on('end', onEnd)
|
|
164
|
+
req.on('error', onError)
|
|
165
|
+
})
|
|
166
|
+
}
|