tux-review 0.1.6 → 0.1.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +27 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tux-review",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "TUX - TUX UIX review system: attach structured, machine-readable feedback directly to running web UIs (pages, routes, components, elements, states) with a deterministic CLI, canonical JSON and LLM-friendly skills.",
5
5
  "license": "MIT",
6
6
  "author": {
package/src/server.js CHANGED
@@ -287,6 +287,22 @@ function static_(state, res, path) {
287
287
  res.end(body);
288
288
  }
289
289
 
290
+ // Transparent proxy response headers (SPC 34): forward every upstream header
291
+ // except the hop-by-hop / length headers this server manages itself. Must
292
+ // produce the same header set as py/tux/server.py::_proxy.
293
+ function proxyHeaders(upHeaders, { html, bodyLength }) {
294
+ const headers = {};
295
+ for (const [k, v] of Object.entries(upHeaders)) {
296
+ const lk = k.toLowerCase();
297
+ if (lk === 'connection' || lk === 'transfer-encoding' || lk === 'content-length') continue;
298
+ if (html && lk === 'cache-control') continue;
299
+ headers[k] = v;
300
+ }
301
+ if (html) headers['cache-control'] = 'no-store';
302
+ headers['content-length'] = String(bodyLength);
303
+ return headers;
304
+ }
305
+
290
306
  function proxy(state, req, res) {
291
307
  const target = new URL(state.target);
292
308
  const options = {
@@ -298,25 +314,17 @@ function proxy(state, req, res) {
298
314
  headers: { ...req.headers, host: target.host },
299
315
  };
300
316
  const upstream = httpRequest(options, (up) => {
301
- const type = up.headers['content-type'] ?? '';
302
- if (type.startsWith('text/html')) {
303
- const chunks = [];
304
- up.on('data', (c) => chunks.push(c));
305
- up.on('end', () => {
306
- // injection lengthens the body — the upstream Content-Length must
307
- // not be forwarded or clients truncate/wait on the response
308
- const body = inject(Buffer.concat(chunks).toString('utf8'));
309
- const headers = { ...up.headers };
310
- delete headers['transfer-encoding'];
311
- headers['content-length'] = String(Buffer.byteLength(body));
312
- headers['cache-control'] = 'no-store';
313
- res.writeHead(up.statusCode, headers);
314
- res.end(body);
315
- });
316
- return;
317
- }
318
- res.writeHead(up.statusCode, up.headers);
319
- up.pipe(res);
317
+ const isHtml = (up.headers['content-type'] ?? '').startsWith('text/html');
318
+ const chunks = [];
319
+ up.on('data', (c) => chunks.push(c));
320
+ up.on('end', () => {
321
+ const raw = Buffer.concat(chunks);
322
+ // Injection lengthens the body, so the upstream Content-Length must be
323
+ // recomputed; non-HTML passes through as raw bytes (no text round-trip).
324
+ const out = isHtml ? Buffer.from(inject(raw.toString('utf8')), 'utf8') : raw;
325
+ res.writeHead(up.statusCode, proxyHeaders(up.headers, { html: isHtml, bodyLength: out.length }));
326
+ res.end(out);
327
+ });
320
328
  });
321
329
  upstream.on('error', (err) => {
322
330
  sendJson(res, 502, { error: { code: 'bad_gateway', message: `target unreachable: ${err.message}` } });