← back to Dw Domain Fleet
Vendor-neutral image proxy: render emits /img/<token>, server.js streams CDN bytes (closed proxy + LRU). Kills image_url vendor-name leak in card/PDP/hero/JSON-LD. DTD-A. Proven local: 0 leaks, /img 200.
bd7303f6e853819a13b30e31cfed36b0a5cbac21 · 2026-06-03 14:14:34 -0700 · Steve Abrams
Files touched
M server.jsM shared/render.js
Diff
commit bd7303f6e853819a13b30e31cfed36b0a5cbac21
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 3 14:14:34 2026 -0700
Vendor-neutral image proxy: render emits /img/<token>, server.js streams CDN bytes (closed proxy + LRU). Kills image_url vendor-name leak in card/PDP/hero/JSON-LD. DTD-A. Proven local: 0 leaks, /img 200.
---
server.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
shared/render.js | 42 ++++++++++++++++++++++++++++++++++++------
2 files changed, 88 insertions(+), 6 deletions(-)
diff --git a/server.js b/server.js
index 1cd8fc5..6916029 100644
--- a/server.js
+++ b/server.js
@@ -54,6 +54,24 @@ const HERO_IMGS = (Array.isArray(HERO_ALLOC[SITE]) && HERO_ALLOC[SITE].length >=
: sortProducts(POOL, 'newest').slice(0, 14).map(p => p.image_url).filter(Boolean);
const FEATURED = sortProducts(POOL, 'newest').slice(0, 20);
+// ---- vendor-neutral image proxy map (built once at boot) ----
+// The raw Shopify CDN image_url embeds the 3rd-party vendor name in the
+// filename; render.js now emits /img/<token> instead. We resolve token -> real
+// CDN url here (render.imgToken is the same pure hash render uses) and the
+// /img/:token route streams the bytes, so the vendor never appears in the DOM
+// OR the browser network tab (DTD 2026-06-03 verdict A — streaming proxy).
+const IMG_MAP = new Map();
+function registerImg(url) { if (url) IMG_MAP.set(render.imgToken(url), url); }
+for (const p of POOL) registerImg(p.image_url); // every card + PDP image
+for (const u of HERO_IMGS) registerImg(u); // homepage hero slides
+console.log(`[${SITE}] image-proxy map: ${IMG_MAP.size} urls`);
+
+// Small per-process LRU of fetched bytes so a hot image isn't re-fetched from
+// the CDN on every request. Capped to keep memory flat across 44+ procs.
+const IMG_CACHE = new Map(); // token -> { buf, type, exp }
+const IMG_CACHE_MAX = 256;
+const IMG_TTL_MS = 6 * 60 * 60 * 1000;
+
const app = express();
// Behind nginx (single reverse-proxy hop). Without this, express-rate-limit sees
// the nginx-set X-Forwarded-For with trust-proxy OFF and throws
@@ -138,6 +156,40 @@ app.get('/buy/:slug', (req, res) => {
+ encodeURIComponent(p.handle) + '#sample');
});
+// Vendor-neutral image proxy. token is a pure hash of the real CDN url (see
+// render.imgToken); we resolve it from the boot map and stream the bytes so the
+// vendor filename never reaches the client. Closed proxy — only urls that were
+// registered at boot (this site's own catalog + hero images) can be fetched, so
+// it can't be abused as an open relay. 404 on unknown token, 502 on upstream
+// failure. Long Cache-Control + a tiny in-proc LRU keep CDN hits rare.
+app.get('/img/:token', async (req, res) => {
+ const token = String(req.params.token || '').slice(0, 64);
+ const url = IMG_MAP.get(token);
+ if (!url) return res.status(404).end();
+ const now = Date.now();
+ const hit = IMG_CACHE.get(token);
+ if (hit && hit.exp > now) {
+ res.setHeader('Content-Type', hit.type);
+ res.setHeader('Cache-Control', 'public, max-age=86400, immutable');
+ res.setHeader('X-Img-Cache', 'HIT');
+ return res.end(hit.buf);
+ }
+ try {
+ const upstream = await fetch(url);
+ if (!upstream.ok) return res.status(502).end();
+ const type = upstream.headers.get('content-type') || 'image/jpeg';
+ const buf = Buffer.from(await upstream.arrayBuffer());
+ if (IMG_CACHE.size >= IMG_CACHE_MAX) IMG_CACHE.delete(IMG_CACHE.keys().next().value);
+ IMG_CACHE.set(token, { buf, type, exp: now + IMG_TTL_MS });
+ res.setHeader('Content-Type', type);
+ res.setHeader('Cache-Control', 'public, max-age=86400, immutable');
+ res.setHeader('X-Img-Cache', 'MISS');
+ return res.end(buf);
+ } catch (e) {
+ return res.status(502).end();
+ }
+});
+
app.get('/about', (req, res) => {
res.type('html').send(render.aboutPage(cfg));
});
diff --git a/shared/render.js b/shared/render.js
index 8c45e34..fcae9c5 100644
--- a/shared/render.js
+++ b/shared/render.js
@@ -2,8 +2,33 @@
* render.js — server-rendered HTML for a fleet microsite.
* All pages share one template; per-domain config drives theme/copy/hero.
*/
+const crypto = require('crypto');
const { dominantHex } = require('./sort');
+/* imgToken / proxyImg — vendor-neutral image URL.
+ * The raw Shopify CDN image_url embeds the 3rd-party vendor name in the
+ * filename (e.g. WolfGordonWallcovering_DWWG_em9505.jpg). Emitting it in a card
+ * data-img / background-image / hero / JSON-LD leaks the vendor (settlement
+ * vendor-confidentiality). The fix is a server-side image proxy: render emits
+ * /img/<token>, and server.js resolves token -> real CDN url from a boot map and
+ * streams the bytes, so the vendor never appears in the DOM OR the network tab
+ * (DTD 2026-06-03 verdict A — streaming proxy, vote 2/2, Qwen non-vote).
+ *
+ * imgToken is a PURE deterministic hash of the url string — render.js stays
+ * stateless (no POOL access). server.js builds its token->url map with this same
+ * function so the two sides agree. sha1 here is an opaque short id, NOT a
+ * security primitive; 20 hex chars = 80 bits, ample for a per-site catalog. */
+function imgToken(url) {
+ if (!url) return '';
+ const ext = (String(url).match(/\.(jpe?g|png|webp|gif|avif)(?:[?#]|$)/i) || [null, 'jpg'])[1].toLowerCase();
+ const h = crypto.createHash('sha1').update(String(url)).digest('hex').slice(0, 20);
+ return h + '.' + (ext === 'jpeg' ? 'jpg' : ext);
+}
+function proxyImg(url) {
+ const t = imgToken(url);
+ return t ? '/img/' + t : '';
+}
+
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, c =>
({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
@@ -349,12 +374,13 @@ function card(p) {
|| scrubVendor((p.sku||p.handle||'').replace(/-Sample$/i,''))
|| 'Designer Wallcovering';
const href = '/product/' + encodeURIComponent(productSlug(p));
+ const img = proxyImg(p.image_url); // vendor-neutral /img/<token> (see imgToken)
return `<div class="card" data-handle="${esc(productSlug(p))}"
data-title="${esc(t)}" data-sku="${esc(scrubVendor(p.sku||p.handle))}"
- data-img="${esc(p.image_url)}" data-tags="${esc(scrubTags(p.tags).slice(0,12).join(', '))}"
+ data-img="${esc(img)}" data-tags="${esc(scrubTags(p.tags).slice(0,12).join(', '))}"
data-price="${esc(p.price)}" data-hex="${esc(hx)}">
<a class="card-link" href="${esc(href)}" aria-label="${esc(t)}">
- <div class="card-img" style="background-image:url('${esc(p.image_url)}');--card-bg:url('${esc(p.image_url)}')"></div>
+ <div class="card-img" style="background-image:url('${esc(img)}');--card-bg:url('${esc(img)}')"></div>
<div class="card-body">
<div class="card-title">${esc(t)}</div>
<div class="card-meta"><span class="dot" style="background:${hx}"></span>
@@ -368,7 +394,9 @@ function card(p) {
function homePage(cfg, heroImgs, featured) {
const title = `${cfg.siteName} | ${cfg.tagline}`;
const desc = clean(cfg.metaDesc);
- const slides = heroImgs.slice(0, 8);
+ // Hero images are raw Shopify CDN urls (vendor in filename) — route them
+ // through the same /img proxy so the homepage hero doesn't leak the vendor.
+ const slides = heroImgs.slice(0, 8).map(proxyImg).filter(Boolean);
const heroHtml = slides.length >= 3
? slides.map((s,i)=>`<div class="cinema-slide${i===0?' on':''}" style="background-image:url('${esc(s)}')"></div>`).join('')
: `<div class="cinema-grid">${slides.concat(slides).slice(0,4).map(s=>`<div style="background-image:url('${esc(s)}')"></div>`).join('')}</div>`;
@@ -692,6 +720,8 @@ function productPage(cfg, p, related) {
|| scrubVendor((p.sku||p.handle||'').replace(/-Sample$/i,''))
|| 'Designer Wallcovering';
const sku = scrubVendor(p.sku || p.handle); // display-only; raw p.handle still used for DW-store links
+ const imgProxy = proxyImg(p.image_url); // /img/<token> — vendor-neutral
+ const imgAbs = imgProxy ? `https://${cfg.domain}${imgProxy}` : ''; // absolute for JSON-LD image
// Self-canonical on the CLEAN slug (vendor-free). Was ${DW}/products/<rawHandle>,
// which leaked 3rd-party vendor names in the rendered <link>/JSON-LD. Buy + main-store
// links now route through /buy/:slug (server resolves the real handle + 302-redirects),
@@ -705,7 +735,7 @@ function productPage(cfg, p, related) {
.map(t => `<span>${esc(t)}</span>`).join('');
const productLd = {
'@context':'https://schema.org','@type':'Product',
- name, sku, image: p.image_url, brand:{ '@type':'Brand', name:'Designer Wallcoverings' },
+ name, sku, image: imgAbs, brand:{ '@type':'Brand', name:'Designer Wallcoverings' },
url: canonical
};
return head(cfg, title, desc, '/product/' + encodeURIComponent(productSlug(p)), canonical)
@@ -787,7 +817,7 @@ function productPage(cfg, p, related) {
<div class="wrap">
<div style="display:grid;grid-template-columns:1.05fr .95fr;gap:48px;padding:28px 0 10px"
class="pdp-grid">
- <div class="card-img" style="background-image:url('${esc(p.image_url)}');
+ <div class="card-img" style="background-image:url('${esc(imgProxy)}');
border-radius:6px;min-height:clamp(360px,52vw,640px)"></div>
<div>
<div style="font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)">
@@ -820,4 +850,4 @@ ${script(cfg)}
</body></html>`;
}
-module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, clean, esc, productSlug, scrubVendor };
+module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };
← 7c6386c Per-site catalog extras overlay (data/extras/<slug>.json) —
·
back to Dw Domain Fleet
·
fix(fleet): route scrubTags through canonical isVendorTag + 45f2fce →