@goodandready/dsh-moa 0.2.53 → 0.2.55

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/README.md CHANGED
@@ -302,6 +302,9 @@ dsh-moa:
302
302
 
303
303
  ## 📊 REST API & Endpoints
304
304
 
305
+ > [!IMPORTANT]
306
+ > **Authentication & Access Control**: All HTTP endpoints enforce fail-closed authentication via the DeepSeek Harness Connection Service (`ctx.connection.requestRejection`). Unauthenticated remote callers over LAN/WAN receive HTTP 401 (`Authentication required`) or 403 (`Forbidden`), strictly preventing disclosure of prompts, workspace structures, and candidate files. Mutation endpoints additionally require loopback origin and CSRF validation (`sec-fetch-site`).
307
+
305
308
  | Endpoint | Method | Description |
306
309
  |:---|:---|:---|
307
310
  | `/dsh-moa/status` | `GET` | Health/enablement snapshot used by the settings card status chip |
@@ -332,7 +335,7 @@ npm test
332
335
  ## 🛠️ Internal Tooling & Development
333
336
 
334
337
  For local verification and package integrity validation:
335
- - `npm test`: runs the full test suite (95 tests)
338
+ - `npm test`: runs the full test suite (288 tests)
336
339
  - `./deploy.sh`: local infrastructure validation script (verifies package size, identity parity in package.json/cordis.patch.yml/client.js, and tests). Excluded from the published npm package.
337
340
 
338
341
  ## 📄 License
package/README.ru.md CHANGED
@@ -302,6 +302,9 @@ dsh-moa:
302
302
 
303
303
  ## 📊 REST API эндпоинты
304
304
 
305
+ > [!IMPORTANT]
306
+ > **Аутентификация и контроль доступа**: Все HTTP-эндпоинты плагина защищены каноническим сервисом соединений DeepSeek Harness (`ctx.connection.requestRejection`). Неаутентифицированные удалённые запросы из сети LAN/WAN отклоняются с кодом HTTP 401 (`Authentication required`) или 403 (`Forbidden`), предотвращая утечку промптов, структуры рабочей директории и файлов кандидатов. Мутирующие эндпоинты дополнительно требуют loopback-вызова и проверки CSRF (`sec-fetch-site`).
307
+
305
308
  | Эндпоинт | Метод | Описание |
306
309
  |:---|:---|:---|
307
310
  | `/dsh-moa/status` | `GET` | Снимок здоровья/включенности для статус-бейджа карточки настроек |
package/lib/access.js ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * DeepSeek Harness Mixture of Agents (MoA) — Access Control & Route Guard (#257)
3
+ */
4
+
5
+ import crypto from 'node:crypto'
6
+ import { assertPathContained } from './file-workspace.js'
7
+
8
+ export function writeJson(res, status, body) {
9
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
10
+ res.end(JSON.stringify(body))
11
+ }
12
+
13
+ export function readBody(req) {
14
+ return new Promise((resolve, reject) => {
15
+ let acc = ''
16
+ req.on('data', (chunk) => {
17
+ acc += chunk
18
+ if (acc.length > 5 * 1024 * 1024) { req.destroy(); reject(new Error('Payload Too Large')) }
19
+ })
20
+ req.on('end', () => resolve(acc))
21
+ req.on('error', reject)
22
+ })
23
+ }
24
+
25
+ export function isAllowedWorkspace(targetCwd, allowedRoots) {
26
+ if (!targetCwd || typeof targetCwd !== 'string') return false
27
+ for (const root of allowedRoots) {
28
+ if (!root) continue
29
+ try {
30
+ assertPathContained(root, targetCwd)
31
+ return true
32
+ } catch {
33
+ // not within root
34
+ }
35
+ }
36
+ return false
37
+ }
38
+
39
+ export function getConnection(ctx) {
40
+ try {
41
+ const fromCtx = ctx?.reflect?.get
42
+ ? (ctx.reflect.get('connection') || ctx?.connection)
43
+ : (ctx?.get ? (ctx.get('connection') || ctx?.connection) : Reflect.get(ctx || {}, 'connection'))
44
+ if (fromCtx) return fromCtx
45
+ if (ctx?.root) return getConnection(ctx.root)
46
+ return undefined
47
+ } catch {
48
+ return undefined
49
+ }
50
+ }
51
+
52
+ export function isLoopbackIp(ip) {
53
+ if (!ip || typeof ip !== 'string') return false
54
+ const clean = ip.trim().toLowerCase().replace(/^::ffff:/, '').replace(/^\[|\]$/g, '')
55
+ return clean === '127.0.0.1' || clean === '::1' || clean === 'localhost'
56
+ }
57
+
58
+ export function cookieNameForAuthority(authority) {
59
+ if (!authority || typeof authority !== 'string') return ''
60
+ const hash = crypto.createHash('sha256').update(authority.trim()).digest('base64url')
61
+ return 'dsh-auth-' + hash
62
+ }
63
+
64
+ export function isLoopbackCookieName(name, host = '') {
65
+ if (!name || typeof name !== 'string' || !name.startsWith('dsh-auth-')) return false
66
+ const authorities = [
67
+ host,
68
+ '127.0.0.1:3000',
69
+ '127.0.0.1:3080',
70
+ '127.0.0.1:3082',
71
+ '127.0.0.1',
72
+ 'localhost:3000',
73
+ 'localhost:3080',
74
+ 'localhost:3082',
75
+ 'localhost',
76
+ '[::1]:3000',
77
+ '[::1]',
78
+ ]
79
+ for (const auth of authorities) {
80
+ if (auth && isLoopbackIp(auth.split(':')[0])) {
81
+ if (cookieNameForAuthority(auth) === name) return true
82
+ }
83
+ }
84
+ return false
85
+ }
86
+
87
+ export function hasClientCredentials(req) {
88
+ if (!req || !req.headers) return false
89
+ const cookie = req.headers.cookie
90
+ if (!cookie || typeof cookie !== 'string') return false
91
+ if (
92
+ cookie.includes('dsh_token=') ||
93
+ cookie.includes('dsh_lan_pin=') ||
94
+ cookie.includes('dsh_auth_session=')
95
+ ) {
96
+ return true
97
+ }
98
+
99
+ const parts = cookie.split(';')
100
+ for (const part of parts) {
101
+ const trimmed = part.trim()
102
+ const eqIdx = trimmed.indexOf('=')
103
+ if (eqIdx === -1) continue
104
+ const name = trimmed.slice(0, eqIdx).trim()
105
+ if (name.startsWith('dsh-auth-')) {
106
+ if (!isLoopbackCookieName(name, req.headers.host)) {
107
+ return true
108
+ }
109
+ }
110
+ }
111
+ return false
112
+ }
113
+
114
+ /**
115
+ * Validates whether an incoming HTTP request is authenticated and admitted (#257).
116
+ *
117
+ * Requirements:
118
+ * 1. CSRF protection: rejects sec-fetch-site: cross-site with 403.
119
+ * 2. Reverse proxy inspection: if request is forwarded with non-loopback x-forwarded-for,
120
+ * verify that the client itself supplied authentic credentials (not just injected loopback proxy cookie).
121
+ * 3. Authoritative DSH connection gate: if connection.requestRejection is present,
122
+ * its verdict (401, 403, or undefined) is authoritative.
123
+ * 4. Network boundary: non-loopback callers without connection service authentication
124
+ * fail closed with 401. Loopback callers are admitted.
125
+ *
126
+ * @param {import('node:http').IncomingMessage} req
127
+ * @param {any} [ctx]
128
+ * @returns {number|undefined} Rejection status (401, 403) or undefined if admitted.
129
+ */
130
+ export function getReadRejectionStatus(req, ctx = null) {
131
+ if (!req || !req.headers) return 403
132
+
133
+ // 1. Strict CSRF protection: reject cross-site requests
134
+ const site = req.headers['sec-fetch-site']
135
+ if (site === 'cross-site') {
136
+ return 403
137
+ }
138
+
139
+ // 2. Reverse-proxy / forwarded client verification (#257)
140
+ const xff = req.headers['x-forwarded-for']
141
+ let isForwardedRemote = false
142
+ if (xff && typeof xff === 'string') {
143
+ const firstIp = xff.split(',')[0].trim()
144
+ if (firstIp && !isLoopbackIp(firstIp)) {
145
+ isForwardedRemote = true
146
+ if (!hasClientCredentials(req)) {
147
+ return 401
148
+ }
149
+ }
150
+ }
151
+
152
+ // 3. Authoritative DSH connection service check (#257)
153
+ const conn = getConnection(ctx) || req?.connectionService || ctx?.connection
154
+ if (conn && typeof conn.requestRejection === 'function') {
155
+ let rejection
156
+ try {
157
+ rejection = conn.requestRejection(req)
158
+ } catch {
159
+ rejection = 403
160
+ }
161
+ if (rejection !== undefined) {
162
+ if (typeof rejection === 'number' && Number.isInteger(rejection) && rejection >= 400 && rejection < 600) {
163
+ return rejection
164
+ }
165
+ return rejection === 401 ? 401 : 403
166
+ }
167
+ return undefined
168
+ }
169
+
170
+ // 4. Network boundary evaluation when connection service is absent
171
+ const rawRemote = req?.socket?.remoteAddress || req?.connection?.remoteAddress || ''
172
+ const cleanRemote = String(rawRemote).trim().toLowerCase().replace(/^::ffff:/, '')
173
+
174
+ if (cleanRemote && !isLoopbackIp(cleanRemote)) {
175
+ return 401
176
+ }
177
+
178
+ if (isForwardedRemote) {
179
+ return 401
180
+ }
181
+
182
+ const host = String(req.headers.host || '').trim().toLowerCase()
183
+ if (!cleanRemote && host) {
184
+ const hostName = host.split(':')[0]
185
+ if (!isLoopbackIp(hostName)) {
186
+ return 401
187
+ }
188
+ }
189
+
190
+ return undefined
191
+ }
192
+
193
+ export function assertAuthorizedRequest(req, res, ctx = null) {
194
+ const status = getReadRejectionStatus(req, ctx)
195
+ if (status !== undefined) {
196
+ writeJson(res, status, {
197
+ ok: false,
198
+ error: status === 401 ? 'Authentication required' : 'Forbidden',
199
+ })
200
+ return false
201
+ }
202
+ return true
203
+ }
package/lib/routes.js CHANGED
@@ -5,7 +5,8 @@
5
5
  import path from 'node:path'
6
6
  import os from 'node:os'
7
7
  import { getMoaHistory, getMoaLeaderboard, getMoaRunById, exportMoaHistory, isTestEnvironment } from './history.js'
8
- import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl, assertPathContained } from './file-workspace.js'
8
+ import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl } from './file-workspace.js'
9
+ import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from './access.js'
9
10
  import { isSafeWriteRequest } from './updater.js'
10
11
  import { runMoAPipeline } from './moa-runner.js'
11
12
  import { generateMoABenchmarkReport } from './moa-report.js'
@@ -14,37 +15,6 @@ import { resolvePresetForPrompt } from './moa-router.js'
14
15
  let _modelsCache = { timestamp: 0, models: [] }
15
16
  const MODELS_CACHE_TTL = 60 * 1000
16
17
 
17
- function writeJson(res, status, body) {
18
- res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
19
- res.end(JSON.stringify(body))
20
- }
21
-
22
- function readBody(req) {
23
- return new Promise((resolve, reject) => {
24
- let acc = ''
25
- req.on('data', (chunk) => {
26
- acc += chunk
27
- if (acc.length > 5 * 1024 * 1024) { req.destroy(); reject(new Error('Payload Too Large')) }
28
- })
29
- req.on('end', () => resolve(acc))
30
- req.on('error', reject)
31
- })
32
- }
33
-
34
- function isAllowedWorkspace(targetCwd, allowedRoots) {
35
- if (!targetCwd || typeof targetCwd !== 'string') return false
36
- for (const root of allowedRoots) {
37
- if (!root) continue
38
- try {
39
- assertPathContained(root, targetCwd)
40
- return true
41
- } catch {
42
- // not within root
43
- }
44
- }
45
- return false
46
- }
47
-
48
18
  export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig, webServerPort, timeMachineEngine, workspaceRoot }) {
49
19
 
50
20
  ctx.effect(() => {
@@ -52,6 +22,7 @@ ctx.effect(() => {
52
22
  kind: 'exact',
53
23
  path: '/dsh-moa/status',
54
24
  handler: (req, res) => {
25
+ if (!assertAuthorizedRequest(req, res, ctx)) return
55
26
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
56
27
  try {
57
28
  const cfg = live()
@@ -72,6 +43,7 @@ ctx.effect(() => {
72
43
  kind: 'exact',
73
44
  path: '/dsh-moa/models',
74
45
  handler: async (req, res) => {
46
+ if (!assertAuthorizedRequest(req, res, ctx)) return
75
47
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
76
48
  if (_modelsCache.models.length > 0 && Date.now() - _modelsCache.timestamp < MODELS_CACHE_TTL) {
77
49
  writeJson(res, 200, { ok: true, models: _modelsCache.models, cached: true })
@@ -168,6 +140,7 @@ ctx.effect(() => {
168
140
  kind: 'exact',
169
141
  path: '/dsh-moa/presets',
170
142
  handler: async (req, res) => {
143
+ if (!assertAuthorizedRequest(req, res, ctx)) return
171
144
  const cfg = live()
172
145
  if (req.method === 'GET') {
173
146
  writeJson(res, 200, {
@@ -237,6 +210,7 @@ ctx.effect(() => {
237
210
  kind: 'exact',
238
211
  path: '/dsh-moa/history',
239
212
  handler: async (req, res) => {
213
+ if (!assertAuthorizedRequest(req, res, ctx)) return
240
214
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
241
215
  try {
242
216
  const u = new URL(req.url, 'http://localhost')
@@ -255,6 +229,7 @@ ctx.effect(() => {
255
229
  kind: 'exact',
256
230
  path: '/dsh-moa/leaderboard',
257
231
  handler: async (req, res) => {
232
+ if (!assertAuthorizedRequest(req, res, ctx)) return
258
233
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
259
234
  try {
260
235
  const url = new URL(req.url || '', 'http://127.0.0.1')
@@ -284,6 +259,7 @@ ctx.effect(() => {
284
259
  kind: 'exact',
285
260
  path: '/dsh-moa/promote',
286
261
  handler: async (req, res) => {
262
+ if (!assertAuthorizedRequest(req, res, ctx)) return
287
263
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
288
264
  if (!isSafeWriteRequest(req)) {
289
265
  writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
@@ -336,6 +312,7 @@ ctx.effect(() => {
336
312
  kind: 'exact',
337
313
  path: '/dsh-moa/diff',
338
314
  handler: async (req, res) => {
315
+ if (!assertAuthorizedRequest(req, res, ctx)) return
339
316
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
340
317
  try {
341
318
  const u = new URL(req.url, 'http://localhost')
@@ -408,6 +385,7 @@ ctx.effect(() => {
408
385
  kind: 'exact',
409
386
  path: '/dsh-moa/history/export',
410
387
  handler: async (req, res) => {
388
+ if (!assertAuthorizedRequest(req, res, ctx)) return
411
389
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
412
390
  try {
413
391
  const url = new URL(req.url || '', 'http://127.0.0.1')
@@ -430,8 +408,9 @@ ctx.effect(() => {
430
408
  ctx.effect(() => {
431
409
  return ctx.webServer.register({
432
410
  kind: 'prefix',
433
- path: '/dsh-moa/runs/',
411
+ path: '/dsh-moa/runs',
434
412
  handler: (req, res) => {
413
+ if (!assertAuthorizedRequest(req, res, ctx)) return
435
414
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
436
415
  const urlPath = req.url.split('?')[0]
437
416
  const parts = urlPath.split('/').filter(Boolean)
@@ -477,6 +456,7 @@ ctx.effect(() => {
477
456
  kind: 'exact',
478
457
  path: '/dsh-moa/route-preset',
479
458
  handler: async (req, res) => {
459
+ if (!assertAuthorizedRequest(req, res, ctx)) return
480
460
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
481
461
  if (!isSafeWriteRequest(req)) {
482
462
  writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
@@ -517,6 +497,7 @@ ctx.effect(() => {
517
497
  kind: 'exact',
518
498
  path: '/dsh-moa/run',
519
499
  handler: async (req, res) => {
500
+ if (!assertAuthorizedRequest(req, res, ctx)) return
520
501
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
521
502
  if (!isSafeWriteRequest(req)) {
522
503
  writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.53",
3
+ "version": "0.2.55",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",