← back to Microsite Engine
chore: lint, refactor, harden (path-traversal guard + readBody leak + esc fleetErr), v0.2.0 (session close)
0714e9ffb423a805ab90195308886986b2dcdff8 · 2026-07-17 07:43:54 -0700 · Steve
Files touched
M package.jsonM public/index.htmlM server.js
Diff
commit 0714e9ffb423a805ab90195308886986b2dcdff8
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 17 07:43:54 2026 -0700
chore: lint, refactor, harden (path-traversal guard + readBody leak + esc fleetErr), v0.2.0 (session close)
---
package.json | 6 ++++--
public/index.html | 2 +-
server.js | 26 ++++++++++++++++----------
3 files changed, 21 insertions(+), 13 deletions(-)
diff --git a/package.json b/package.json
index 7010ba9..dfde731 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,10 @@
{
"name": "microsite-engine",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"description": "Config-generator dashboard that drives the dw-domain-fleet — preview a niche live against the real catalog, emit a deploy-ready sites/<slug>.json. Never writes the live fleet, never deploys.",
"main": "server.js",
- "scripts": { "start": "node server.js" }
+ "scripts": {
+ "start": "node server.js"
+ }
}
diff --git a/public/index.html b/public/index.html
index ccd4920..f28ede8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -173,7 +173,7 @@ async function boot(){
FACETS = await (await fetch('/api/facets')).json();
$('fleetBadge').textContent = FACETS.fleetOk ? (FACETS.total.toLocaleString()+' products · fleet linked') : 'FLEET NOT LINKED';
if(!FACETS.fleetOk){
- $('fleetErr').innerHTML = '<div class="banner">Could not load dw-domain-fleet shared modules: '+(FACETS.fleetErr||'')+'<br>Preview will be empty until the fleet is reachable.</div>';
+ $('fleetErr').innerHTML = '<div class="banner">Could not load dw-domain-fleet shared modules: '+esc(FACETS.fleetErr||'')+'<br>Preview will be empty until the fleet is reachable.</div>';
}
$('palette').innerHTML = FACETS.palettes.map(p=>`<option>${p}</option>`).join('');
$('font').innerHTML = FACETS.fonts.map(f=>`<option>${f}</option>`).join('');
diff --git a/server.js b/server.js
index 4cf744a..ad714e9 100644
--- a/server.js
+++ b/server.js
@@ -17,10 +17,13 @@ const fs = require('fs');
const path = require('path');
const { URL } = require('url');
-const PORT = parseInt(process.env.PORT || '9770', 10);
-const FLEET = process.env.FLEET_DIR || path.join(process.env.HOME, 'Projects', 'dw-domain-fleet');
-const OUTPUT = path.join(__dirname, 'output');
-const DRAFTS = path.join(__dirname, 'data', 'drafts.json');
+const PORT = parseInt(process.env.PORT || '9770', 10);
+const FLEET = process.env.FLEET_DIR || path.join(process.env.HOME, 'Projects', 'dw-domain-fleet');
+const OUTPUT = path.join(__dirname, 'output');
+const DRAFTS = path.join(__dirname, 'data', 'drafts.json');
+const PORT_BASE = 10001; // first candidate port for new fleet sites
+const PREVIEW_MAX = 4000; // nicheSlice ceiling for preview
+const PREVIEW_GRID = 60; // max cards rendered in the preview grid
fs.mkdirSync(OUTPUT, { recursive: true });
fs.mkdirSync(path.dirname(DRAFTS), { recursive: true });
@@ -73,7 +76,7 @@ function liveSites() {
}
function nextFreePort(live, drafts) {
const used = new Set([...live, ...drafts].map(s => s.port).filter(Boolean));
- let p = 10001;
+ let p = PORT_BASE;
while (used.has(p)) p++;
return p;
}
@@ -137,9 +140,9 @@ function buildConfig(body, port) {
// ---- preview: run the REAL nicheSlice + sortProducts ----
function preview(body) {
const niche = { pos: cleanList(body.pos), neg: cleanList(body.neg), types: cleanList(body.types) };
- const all = catalog.nicheSlice({ ...niche, limit: 4000 });
+ const all = catalog.nicheSlice({ ...niche, limit: PREVIEW_MAX });
const sorted = sortMod.sortProducts(all, body.sort || 'newest');
- const cards = sorted.slice(0, 60).map(p => ({
+ const cards = sorted.slice(0, PREVIEW_GRID).map(p => ({
title: p.title,
image_url: p.image_url,
product_type: p.product_type,
@@ -179,8 +182,9 @@ function json(res, code, obj) { send(res, code, 'application/json', JSON.stringi
function readBody(req) {
return new Promise((resolve) => {
let b = '';
- req.on('data', c => { b += c; if (b.length > 1e6) req.destroy(); });
+ req.on('data', c => { b += c; if (b.length > 1e6) { req.destroy(); resolve({}); } });
req.on('end', () => { try { resolve(JSON.parse(b || '{}')); } catch { resolve({}); } });
+ req.on('error', () => resolve({}));
});
}
@@ -243,9 +247,11 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/api/delete-draft' && req.method === 'POST') {
const body = await readBody(req);
- const drafts = loadDrafts().filter(d => d.slug !== body.slug);
+ const slug = String(body.slug || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
+ if (!slug) return json(res, 400, { error: 'slug required' });
+ const drafts = loadDrafts().filter(d => d.slug !== slug);
saveDrafts(drafts);
- try { fs.unlinkSync(path.join(OUTPUT, body.slug + '.json')); } catch {}
+ try { fs.unlinkSync(path.join(OUTPUT, slug + '.json')); } catch {}
return json(res, 200, { ok: true });
}
return send(res, 404, 'text/plain', 'not found');
← c509c2b microsite-engine: live rotating-hero preview + companion her
·
back to Microsite Engine
·
microsite-engine: prefilled dropdowns + typed autocomplete o ed286d5 →