← back to Ffepurchasing
step2 vendor-leak fix: dual-key /sample resolver + handle_display hrefs
1ec14dba8f87c2ed6e764b09bed167d884e9fd25 · 2026-06-09 13:47:50 -0700 · Steve
Files touched
M public/index.htmlM server.js
Diff
commit 1ec14dba8f87c2ed6e764b09bed167d884e9fd25
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 9 13:47:50 2026 -0700
step2 vendor-leak fix: dual-key /sample resolver + handle_display hrefs
---
public/index.html | 10 +++++-----
server.js | 35 ++++++++++++++++++++++++++++++++++-
2 files changed, 39 insertions(+), 6 deletions(-)
diff --git a/public/index.html b/public/index.html
index 5ea57bb..8e0691b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -640,7 +640,7 @@ function tradeSpec(p) {
const trade = (p.trade_price != null) ? Number(p.trade_price) :
(p.max_price != null) ? Number(p.max_price) : null;
return {
- sku: p.sku || p.handle || '',
+ sku: p.sku || p.handle_display || p.handle || '',
width: p.width || null,
repeat: p.repeat || p.vertical_repeat || null,
moq: (moq && Number(moq) > 0) ? Number(moq) : null,
@@ -664,13 +664,13 @@ function cardHTML(p) {
+ '<div class="pat">' + escAttr(p.pattern_name || p.title) + '</div>'
+ '<div class="trade">' + trade + '</div>'
+ '<div class="actions">'
- + '<button class="sample-btn" onclick="event.stopPropagation();dwmOpen(\'Sample\',' + JSON.stringify({sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}).replace(/"/g,'"') + ')">Sample</button>'
+ + '<button class="sample-btn" onclick="event.stopPropagation();dwmOpen(\'Sample\',' + JSON.stringify({sku:p.sku||p.handle_display||p.handle, title:p.title, image_url:safeImg(p.image_url)}).replace(/"/g,'"') + ')">Sample</button>'
+ '</div></div>';
}
function rowHTML(p) {
const t = tradeSpec(p);
- const optsAttr = JSON.stringify({sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}).replace(/"/g,'"');
+ const optsAttr = JSON.stringify({sku:p.sku||p.handle_display||p.handle, title:p.title, image_url:safeImg(p.image_url)}).replace(/"/g,'"');
return '<tr>'
+ '<td><img class="timg" src="' + escAttr(safeImg(p.image_url)) + '" alt="" loading="lazy"></td>'
+ '<td class="tsku">' + escAttr(t.sku || '—') + '</td>'
@@ -759,7 +759,7 @@ async function loadGridPage() {
a.classList.add('revealed');
return;
}
- dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)});
+ dwmOpen('Sample',{sku:p.sku||p.handle_display||p.handle, title:p.title, image_url:safeImg(p.image_url)});
};
a.innerHTML = cardHTML(p);
grid.appendChild(a);
@@ -1102,7 +1102,7 @@ loadGridPage();
const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
function card(p) {
const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
- return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle_display || p.handle || p.sku) + '">' +
'<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
'<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
'</a>';
diff --git a/server.js b/server.js
index ba1e699..6677e51 100644
--- a/server.js
+++ b/server.js
@@ -199,8 +199,40 @@ app.get('/trade-account', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'trade-account.html'));
});
+// Step 2 of the vendor handle/sku text-leak fix (2026-06-09): the client's
+// /sample/ hrefs now point at the vendor-scrubbed `handle_display` slug
+// (emitted by _shared/api-vendor-redact since Step 1), so the raw vendor-bearing
+// handle never rides in the URL bar. This route is DUAL-KEY — it resolves the raw
+// handle/sku FIRST (old bookmarks + buy path, byte-identical behavior), then falls
+// back to a redacted->product map built after catalog load. If two raw handles
+// redact to the same display slug, that slug is logged and left raw-only.
+const { redactVendorText } = require('../_shared/vendor-text-redact');
+let SAMPLE_DISPLAY = new Map(); // redacted display slug -> product
+function buildSampleDisplayMap() {
+ const map = new Map();
+ const collided = new Set();
+ for (const p of PRODUCTS_NICHE) {
+ for (const key of [p.handle, p.sku]) {
+ if (!key) continue;
+ const d = redactVendorText(String(key));
+ if (!d || d === key) continue; // nothing redacted — raw route already matches
+ const prev = map.get(d);
+ if (prev && prev !== p) { collided.add(d); continue; }
+ map.set(d, p);
+ }
+ }
+ for (const d of collided) {
+ map.delete(d);
+ console.warn(`[ffepurchasing] /sample display-slug collision — keeping raw-only for "${d}"`);
+ }
+ SAMPLE_DISPLAY = map;
+ console.log(`[ffepurchasing] /sample dual-key map: ${map.size} display slugs, ${collided.size} collisions (raw-only)`);
+}
+
app.get('/sample/:handle', (req, res) => {
- const p = PRODUCTS_NICHE.find(x => x.handle === req.params.handle || x.sku === req.params.handle);
+ const key = req.params.handle;
+ const p = PRODUCTS_NICHE.find(x => x.handle === key || x.sku === key) // raw first — old behavior intact
+ || SAMPLE_DISPLAY.get(key); // then the redacted display form
if (!p) return res.status(404).send('Not found');
res.redirect(302, p.product_url || `${DW_SHOPIFY}/products/${encodeURIComponent(p.handle)}#sample`);
});
@@ -247,6 +279,7 @@ if (catalog) catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
}
PRODUCTS_NICHE = PRODUCTS.filter(nicheFit);
console.log(`[${__SITE}] niche filter: kept ${PRODUCTS_NICHE.length} of ${PRODUCTS.length}`);
+ buildSampleDisplayMap(); // Step 2: redacted->raw /sample resolver keys
app.listen(PORT, '127.0.0.1', () => {
console.log(`ffepurchasing listening on http://127.0.0.1:${PORT}`);
});
← 004be02 snapshot before step2 codemod
·
back to Ffepurchasing
·
ffepurchasing: rails fire again — wire sliders + grid filter 2e367c7 →