`vite.config.ts`: **2536 -> 141 Zeilen**. Der Dev-Server liegt jetzt in
`dev-server/` (17 Module) mit 44 eigenen Tests. Die Build-Konfiguration eines
Projekts ist die eine Datei, die jeder anfassen muss und niemand testen kann;
darin steckte ein vollstaendiger HTTP-Server mit Prozessverwaltung.
Die Schnitte folgen GETEILTEM ZUSTAND, nicht Zeilenzahl — `comfy.ts` haelt
einen veraenderlichen Kindprozess samt Logpuffer, `downloads.ts` eine Map
laufender Downloads, `whisper.ts` Prozess und Antwortschlange, `web-search.ts`
einen Verfuegbarkeitsstatus, den Status, Installer und Suchkette teilen. Was
zwei Unabhaengige brauchen (`ssrf.ts`, `python.ts`, `paths.ts`), liegt
daneben statt in einem von beiden. `index.ts` traegt nur noch die Reihenfolge.
Der Port ist ein Parameter: `LU_DEV_PORT` -> `devServerPlugin({ port })`.
Ein Server, dessen Port fest verdrahtet ist, laesst sich nicht zweimal
starten und damit auch nicht pruefen.
Die 15 Lint-Fehler der Datei sind bezahlt, nicht unterdrueckt: 12
`require()`, von denen jedes einen Import SCHATTETE, der oben in derselben
Datei schon stand, und 3 leere `catch` mit dem Grund im Block (Hausstil,
Vorbild `src/api/discover.ts:657`). eslint . faellt damit von 60 auf 45 —
die verbleibenden liegen alle in `src/components`.
Geprueft im eigenen Worktree auf HEAD (nur diese Dateien, node_modules
kopiert statt verlinkt): tsc -b 0 · vite build 0 · madge 0 Zyklen ueber
`src` UND `dev-server` · eslint 45 · vitest 7907 gruen / 0 rot.
Zwei eigene Gegenproben zusaetzlich zu den 12 Sonden des Agenten:
1. ENDPUNKT-PARITAET, mengenweise statt gezaehlt: alle `/local-api/`-Pfade
aus HEADs vite.config.ts gegen alle aus dev-server/ — 46 zu 46, und die
Differenz ist in BEIDE Richtungen leer. Kein Endpunkt verloren, keiner
erfunden.
2. Die REIHENFOLGE, die der Agent als folgenlos meldet. Drei Pfade sind
Praefix eines anderen (`download-model` < `download-model-to-path`,
`fs-read` < `fs-read-bytes`, `transcribe` < `transcribe-status`) — dort
waere ein zu weit greifender Mount ein echter Fehler. Am laufenden Server
auf Port 5401 gemessen: `/transcribe-status` -> **200** (eigener Handler),
`/transcribe` -> 405 (nur POST). Haette der Praefix-Mount zugegriffen,
stuende dort 405. Ohne CSRF-Kopfzeile -> 403, die Sperre greift.
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from 'http'
|
|
import { JailEscapeError } from '../src/lib/dev-fs-jail'
|
|
import { decodeBodyChunks, parseJsonBody } from '../src/dev/http-body'
|
|
import { errorText } from '../src/types/json-guards'
|
|
|
|
// ── Request bodies ──────────────────────────────────────────────
|
|
// Every /local-api POST handler used to open with the same three lines:
|
|
//
|
|
// let body = ''
|
|
// req.on('data', (c: any) => { body += c })
|
|
// req.on('end', () => { const { path } = JSON.parse(body) … })
|
|
//
|
|
// Twenty copies, and two defects copied twenty times with them: `body += c`
|
|
// decodes each chunk on its own (a multi-byte character split across a chunk
|
|
// boundary arrives as U+FFFD), and three of the handlers called JSON.parse
|
|
// with no try — a throw inside an 'end' listener happens long after the
|
|
// middleware returned, so nothing catches it and a single malformed POST takes
|
|
// the whole `npm run dev` process down with it. Both fixes live in
|
|
// src/dev/http-body.ts, next to their test; this is the one call site.
|
|
export type JsonBodyHandler = (body: unknown) => void
|
|
|
|
export function withJsonBody(req: IncomingMessage, res: ServerResponse, handle: JsonBodyHandler): void {
|
|
const chunks: Buffer[] = []
|
|
req.on('data', (chunk: Buffer) => { chunks.push(chunk) })
|
|
req.on('end', () => {
|
|
const parsed = parseJsonBody(decodeBodyChunks(chunks))
|
|
if (!parsed.ok) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ error: `Invalid JSON body: ${parsed.error}` }))
|
|
return
|
|
}
|
|
try {
|
|
handle(parsed.value)
|
|
} catch (err) {
|
|
// Same reason as above: we are past the middleware, nobody else is left
|
|
// to catch this. A 400 beats a dead dev server.
|
|
res.writeHead(err instanceof JailEscapeError ? 403 : 400, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ error: errorText(err) || String(err) }))
|
|
}
|
|
})
|
|
}
|
|
|
|
/** POST-only endpoints answer 405 to everything else, as they always did. */
|
|
export function requirePost(req: IncomingMessage, res: ServerResponse): boolean {
|
|
if (req.method === 'POST') return true
|
|
res.writeHead(405)
|
|
res.end()
|
|
return false
|
|
}
|
|
|
|
/** One JSON response, the shape every handler here writes. */
|
|
export function sendJson(res: ServerResponse, status: number, payload: unknown): void {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify(payload))
|
|
}
|
|
|
|
/**
|
|
* The error answer for a handler that resolves a caller-supplied path: a jail
|
|
* escape is a 403 and says so, everything else keeps the 400 these endpoints
|
|
* always answered with.
|
|
*/
|
|
export function failRequest(res: ServerResponse, err: unknown): void {
|
|
sendJson(res, err instanceof JailEscapeError ? 403 : 400, { error: errorText(err) || String(err) })
|
|
}
|